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 c41fb4646a..ca1bc79510 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,24 +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 @@ -32,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 @@ -42,178 +94,295 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build: - name: build-${{ matrix.os }}-${{ matrix.ghc }} - 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 + - os: 22.04 ghc: "8.10.7" - cache_path: ~/.cabal/store - - os: ubuntu-20.04 - ghc: "9.6.3" - 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 - ghc: "9.6.3" - cache_path: ~/.cabal/store - asset_name: simplex-chat-ubuntu-22_04-x86-64 + 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 - ghc: "9.6.3" - cache_path: ~/.cabal/store - asset_name: simplex-chat-macos-aarch64 - desktop_asset_name: simplex-desktop-macos-aarch64.dmg - - os: macos-13 - ghc: "9.6.3" - cache_path: ~/.cabal/store - asset_name: simplex-chat-macos-x86-64 - desktop_asset_name: simplex-desktop-macos-x86_64.dmg - - os: windows-latest - ghc: "9.6.3" - 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: ${{ matrix.ghc }} - cabal-version: "3.10.1.0" + swap-size-gb: 30 + + # 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 - id: restore_cache - uses: actions/cache/restore@v3 + if: matrix.should_run == true + uses: actions/cache@v4 with: path: | - ${{ matrix.cache_path }} + ~/.cabal/store dist-newstyle - key: ${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ 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 simplexmq" >> cabal.project.local - echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local - echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local - echo "" >> cabal.project.local - echo "package direct-sqlcipher" >> cabal.project.local - echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local - echo " extra-lib-dirs: /opt/homebrew/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: Unix prepare cabal.project.local for Mac - if: matrix.os == 'macos-13' - shell: bash - run: | - echo "ignore-project: False" >> cabal.project.local - echo "package simplexmq" >> 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 "" >> 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 - - - name: Install AppImage dependencies - if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04' - run: sudo apt install -y desktop-file-utils - - - name: Install pkg-config for Mac - if: matrix.os == 'macos-latest' || matrix.os == 'macos-13' - 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.asset_name && 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.asset_name && 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') && matrix.asset_name - 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.asset_name && (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.asset_name && 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' || matrix.os == 'macos-13') + if: startsWith(github.ref, 'refs/tags/v') shell: bash env: APPLE_SIMPLEX_SIGNING_KEYCHAIN: ${{ secrets.APPLE_SIMPLEX_SIGNING_KEYCHAIN }} @@ -223,88 +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.asset_name && (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.asset_name && (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.asset_name && 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.asset_name && 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' || matrix.os == 'macos-13') - 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' || matrix.os == 'macos-13') - 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: Cache unix build - uses: actions/cache/save@v3 - if: matrix.os != 'windows-latest' - with: - path: | - ${{ matrix.cache_path }} - dist-newstyle - key: ${{ steps.restore_cache.outputs.cache-primary-key }} - - - name: Unix test - if: matrix.os != 'windows-latest' - timeout-minutes: 40 + - 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 @@ -316,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:$(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 @@ -334,70 +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:$(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 + echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $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 + - name: Upload Desktop + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.windows_desktop_build.outputs.package_path }} - asset_name: ${{ matrix.desktop_asset_name }} - tag: ${{ github.ref }} - - - 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 }} - with: - append_body: true - body: | - ${{ steps.windows_desktop_build.outputs.package_hash }} - - - name: Cache windows build - uses: actions/cache/save@v3 - if: matrix.os == 'windows-latest' - with: - path: | - ${{ matrix.cache_path }} - dist-newstyle - key: ${{ steps.restore_cache.outputs.cache-primary-key }} - - # 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/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 7fc66308f8..5fbe8293bc 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -10,6 +10,7 @@ on: - blog/** - docs/** - .github/workflows/web.yml + - PRIVACY.md jobs: build: @@ -32,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 e3ea5d267b..645b55ec9d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ 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 6c60195f97..cdcbc40d7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local # Compile simplex-chat RUN cabal update -RUN cabal build exe:simplex-chat +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) && \ 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 3b4ba2ba8a..18e5539726 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,64 +1,95 @@ --- layout: layouts/privacy.html +permalink: /privacy/index.html --- -# SimpleX Chat Privacy Policy and Conditions of Use +# SimpleX Chat Operators Privacy Policy and Conditions of Use -SimpleX Chat 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. +## Summary -SimpleX Chat communication 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 Chat apps allow their users to send messages and files via relay server infrastructure. Relay server owners and providers 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 provide any user accounts. +[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 our mission or values, 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). +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 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 cannot be 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), unlike most other communication platforms, services and networks. +### General principles -SimpleX Chat 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 any kind of addresses or other 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. +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 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 providers, 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 preset servers that we operate, as explained below. We are exploring the solutions to deliver push notifications to iOS devices via other providers or users' own servers. +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. -While SimpleX Chat Ltd is not a communication service provider, and provide public preset relays "as is", as experimental, without any guarantees of availability or data retention, we are committed to maintain a high level of availability, reliability and security of these preset relays. We will be adding alternative preset infrastructure providers to the software in the future, and you will continue to be able to use any other providers or your own servers. +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. -We see users and data sovereignty, and device and provider portability as critically important properties for any communication system. +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 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 – see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +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 -Servers used by SimpleX Chat apps do not create, store or identify user profiles. The profiles you can create in the app are local to your device, and can be removed at any time via the app. +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 the local profile, no records are created on any of the relay servers, and infrastructure providers, whether SimpleX Chat Ltd 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. +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 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 8mb 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. +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, 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. -You do not have control over which servers are used to send messages to your contacts - they are chosen by them. To send messages your client needs to connect to these servers, therefore the servers chosen by your contacts can observe your IP address. You can use VPN or some overlay network (e.g., Tor) to hide your IP address from the servers chosen by your contacts. In the near future we will add the layer in the messaging protocol that will route sent message via the relays chosen by you as well. +#### Private message delivery -The messages are permanently removed from the used relay servers as soon as they are delivered, as long as these servers used unmodified published code. Undelivered messages are deleted after the time that is configured in the messaging servers you use (21 days for preset messaging 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. + +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). -If a messaging servers are restarted, the encrypted message can be stored in a backup file until it is overwritten by the next restart (usually within 1 week for preset relay servers). - -As this software is fully open-source and provided under AGPLv3 license, all infrastructure providers and owners, 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 provided servers. - -In addition to the AGPLv3 license terms, SimpleX Chat Ltd is committed to the software users that the preset relays that we provide via the apps will always be compiled from the [published open-source code](https://github.com/simplex-chat/simplexmq), without any modifications. +The encrypted messages can be stored for some time after they are delivered or expired (because servers use append-only logs for message storage). This time varies, and may be longer in connections with fewer messages, but it is usually limited to 1 month, including any backup storage. #### Connections with other users -When you create a connection with another user, two messaging queues (you can think about them as mailboxes) are created on messaging relay servers (chosen by you and your contact each), that can be the preset servers or the servers that you and your contact configured in the app. SimpleX messaging protocol uses separate queues for direct and response messages, and the apps prefer to create these queues on two different relay servers for increased privacy, in case you have more than one relay 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. -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 providers 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 @@ -74,101 +105,142 @@ You can always safely replace the initial part of the link `https://simplex.chat #### 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. 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. -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 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. +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 +This section applies only to the experimental group directory operated by SimpleX Chat Ltd. + [SimpleX Directory](/docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). +#### Public groups and content channels + +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. + +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. + #### User Support -If you contact SimpleX Chat Ltd, any personal data you 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, and avoid sharing any personal information. +The app includes support contact operated by SimpleX Chat Ltd. If you contact support, any personal data you share is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://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. -### Information we may share +### Preset Server Operators -SimpleX Chat Ltd operates preset relay servers using third parties. While we 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 our servers. Hosting providers can also store IP addresses and other transport information as part of their logs. +Preset server operators will not share the information on their servers with each other, other than aggregate usage statistics. -We use 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, we recommend contacting us via SimpleX Chat or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat). +Preset server operators must not provide general access to their servers or the data on their servers to each other. -The cases when SimpleX Chat Ltd may share the data temporarily stored on the servers: +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 software users, SimpleX Chat Ltd, 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 the preset relay servers or any information from the servers by any third parties. If we are ever requested to provide such access or information, we will follow the due legal process to limit any information shared with the third parties to the minimally required by law. +By the time of updating this document, the preset server operators were not served with any enforceable requests and did not provide any information from the servers to any third parties. If the preset server operators are ever requested to provide such access or information, they will follow the due legal process to limit any information shared with the third parties to the minimally required by law. -We will publish information we are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md). +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 software applications and preset relays infrastructure 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 Conditions of Use of Software and Infrastructure 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. + +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). ## Conditions of Use of Software and Infrastructure -You accept the Conditions of Use of Software and Infrastructure ("Conditions") by installing or using any of our software or using any of our server infrastructure (collectively referred to as "Applications"), whether preset in the software or not. +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. -**Minimal age**. You must be at least 13 years old to use our Applications. The minimum age to use our Applications without parental approval may be higher in your country. +**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. -**Infrastructure**. Our Infrastructure includes preset messaging and file relay servers, and iOS push notification servers provided by SimpleX Chat Ltd for public use. Our 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. +**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. -**Client applications**. Our 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 our 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 information with SimpleX Chat Ltd or any other third parties. If you ever discover any tracking or analytics code, please report it to us, so we can remove it. +**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. -**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 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. +**Accessing the infrastructure**. For the efficiency of the network access, the client Software by default accesses all queues your app creates on any relay server within one user profile via the same network (TCP/IP) connection. At the cost of additional traffic this configuration can be changed to use different transport session for each connection. Relay servers do not collect information about which queues were created or accessed via the same connection, so the relay servers cannot establish which queues belong to the same user profile. Whoever might observe your network traffic would know which relay servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common, even inside TLS encryption layer. Please refer to the [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about the privacy model and known security and privacy risks. -**Privacy of user data**. Servers do not retain any data we transmit for any longer than necessary to deliver the messages between apps. SimpleX Chat Ltd collects aggregate statistics across all its servers, as supported by published code and can be enabled by any infrastructure provider, but not any statistics per-user, or per geographic location, or per IP address, or per transport session. We do not have information about how many people use SimpleX Chat applications, we only know an approximate number of app installations and the aggregate traffic through the preset servers. In any case, we do not and will not sell or in any way monetize user data. Our future business model assumes charging for some optional Software features instead, in a transparent and fair way. +**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. -**Operating our Infrastructure**. For the purpose of using our 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 we have or use facilities and service providers or partners. The information about geographic location of the servers will be made available in the apps in the near future. +**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. -**Software**. You agree to downloading and installing updates to our Applications when they are available; they would only be automatic if you configure your devices in this way. +**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. -**Traffic and device costs**. You are solely responsible for the traffic and device costs that you incur while using our Applications, and any associated taxes. +**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. -**Legal usage**. You agree to use our Applications only for legal purposes. You will not use (or assist others in using) our Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal communications, e.g. spam. While we cannot access content or identify messages or groups, in some cases the links to the illegal communications available via our Applications can be shared publicly on social media or websites. We reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via our servers, whether they were reported by the users or discovered by our team. +**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. -**Damage to SimpleX Chat Ltd**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit our Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, our Infrastructure, or any other systems. For example, you must not 1) access our Infrastructure or systems without authorization, in any way other than by using the Software; 2) disrupt the integrity or performance of our Infrastructure; 3) collect information about our users in any manner; or 4) sell, rent, or charge for our 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. +**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. -**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. +**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. + +**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. **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. **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. -**No Access to Emergency Services**. Our 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. +**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. -**Third-party services**. Our Applications may allow you to access, use, or interact with our or 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. +**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. -**Your Rights**. You own the messages and the information you transmit through our 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. +**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. -**License**. SimpleX Chat Ltd grants you a limited, revocable, non-exclusive, and non-transferable license to use our 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). +**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). -**SimpleX Chat Ltd Rights**. We own all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with our Applications. 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. +**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. -**Disclaimers**. YOU USE OUR APPLICATIONS AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. WE PROVIDE OUR 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 US IS ACCURATE, COMPLETE, OR USEFUL, THAT OUR APPLICATIONS WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT OUR APPLICATIONS WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. WE DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN OUR USERS USE OUR APPLICATIONS. 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. +**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. -**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 OUR APPLICATIONS, 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 CONDITIONS, US, OR OUR 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 OUR CONDITIONS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW. +**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. -**Availability**. Our Applications may be disrupted, including for maintenance, upgrades, or network or equipment failures. We may discontinue some or all of our Applications, including certain features and the support for certain devices and platforms, at any time. +**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. -**Resolving disputes**. You agree to resolve any Claim you have with us relating to or arising from our Conditions, us, or our 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 our Conditions, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat Ltd and you, without regard to conflict of law provisions. +**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. Your continued use of our Applications confirms your acceptance of our updated Conditions and supersedes any prior Conditions. You will comply with all applicable export control and trade sanctions laws. Our Conditions cover the entire agreement between you and SimpleX Chat Ltd regarding our Applications. If you do not agree with our Conditions, you should stop using our Applications. +**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 we fail to enforce any of our Conditions, that does not mean we 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 our Conditions and shall not affect the enforceability of the remaining provisions. Our Applications 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 Applications in any country. If you have specific questions about these Conditions, please contact us at chat@simplex.chat. +**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 at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. 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. +**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 April 24, 2024 +Updated March 3, 2025 diff --git a/README.md b/README.md index 697f5ca08e..554c6068d9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # 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,7 +72,7 @@ You must: Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment. -You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=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) +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-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: @@ -83,7 +83,7 @@ There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=s There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users: -[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-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. @@ -141,16 +150,7 @@ Join our translators to help SimpleX grow! 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! @@ -158,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, @@ -234,30 +235,28 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: -[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md) +[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) -[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) +[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) -[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: Quick installation of a terminal app @@ -297,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. +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 @@ -382,10 +384,12 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A - ✅ 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. +- 🏗 Short links to connect and join groups. - 🏗 Improve stability and reduce battery usage. - 🏗 Improve experience for the new users. -- 🏗 Large groups, communities and public channels. -- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). - Privacy & security slider - a simple way to set all settings at once. - SMP queue redundancy and rotation (manual is supported). - Include optional message into connection request sent via contact address. @@ -404,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. diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 7204625ad4..3f6998c9ec 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -9,39 +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() } - NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil) 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 - } - - @objc func pasteboardChanged() { - ChatModel.shared.pasteboardHasStrings = UIPasteboard.general.hasStrings - } - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02hhx", $0) }.joined() logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)") @@ -75,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))") @@ -141,6 +120,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { } } + private func prepareForLaunch() { + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + } + static func keepScreenOn(_ on: Bool) { UIApplication.shared.isIdleTimerDisabled = on } @@ -148,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 acea38e69e..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,14 +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 = 50 + private let callTopPadding: CGFloat = 40 private enum ChatListActionSheet: Identifiable { case planAndConnectSheet(sheet: PlanAndConnectActionSheet) @@ -51,6 +65,16 @@ 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. @@ -74,7 +98,7 @@ struct ContentView: View { 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 @@ -97,9 +121,6 @@ struct ContentView: View { } } .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 } @@ -138,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 { @@ -177,14 +209,14 @@ struct ContentView: View { } } - @ViewBuilder private func activeCallInteractiveArea(_ call: Call) -> some 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 - 10) + .frame(height: callTopPadding) .background(Color(uiColor: UIColor(red: 47/255, green: 208/255, blue: 88/255, alpha: 1))) .onTapGesture { chatModel.activeCallViewIsCollapsed = false @@ -224,13 +256,14 @@ 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 @@ -239,17 +272,31 @@ struct ContentView: View { 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() @@ -259,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) { @@ -277,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) + } } } } @@ -372,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( @@ -394,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/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index a4651e1d42..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,11 +52,224 @@ 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? @@ -55,20 +277,21 @@ final class ChatModel: ObservableObject { @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] = [] + @Published private(set) var chats: [Chat] = [] @Published var deletedChats: Set = [] - // map of connections network statuses, key is agent connection id - @Published var networkStatuses: Dictionary = [:] // 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] = [] @@ -78,6 +301,7 @@ 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? @@ -100,10 +324,9 @@ final class ChatModel: ObservableObject { @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 pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings @Published var networkInfo = UserNetworkInfo(networkType: .other, online: true) + // usage conditions + @Published var conditions: ServerOperatorConditions = .empty var messageDelivery: Dictionary Void> = [:] @@ -111,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 @@ -176,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) { @@ -222,6 +476,7 @@ final class ChatModel: ObservableObject { updateChatInfo(cInfo) } else if addMissing { addChat(Chat(chatInfo: cInfo, chatItems: [])) + ChatTagsModel.shared.addPresetChatTags(cInfo, ChatStats()) } } @@ -245,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])) } @@ -315,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 { @@ -326,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 } @@ -357,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 { @@ -366,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) { @@ -387,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 @@ -422,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 } @@ -433,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 @@ -565,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) @@ -581,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) } @@ -591,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) } @@ -604,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) @@ -617,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) } } @@ -627,7 +1042,7 @@ final class ChatModel: ObservableObject { } func dismissConnReqView(_ id: String) { - if id == showingInvitation?.connId { + if id == showingInvitation?.pcc.id { markShowingInvitationUsed() dismissAllSheets() } @@ -639,7 +1054,11 @@ final class ChatModel: ObservableObject { 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) + } } } @@ -651,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 { @@ -675,46 +1097,26 @@ 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 connId: String + var pcc: PendingContactConnection var connChatUsed: Bool } @@ -723,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 @@ -754,49 +1152,18 @@ 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 - case .local: - return true - 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)" } } - 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 - } - } else { - return true - } - } - public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) } 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 b4c9a48d5d..d92411decd 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -11,46 +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) } } } @@ -92,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, _ ctrl: chat_ctrl? = nil) -> ChatResponse { - logger.debug("chatSendCmd \(cmd.cmdType)") +@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)") + } let start = Date.now - let resp = bgTask + let resp: APIResult = bgTask ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) } : sendSimpleXCmd(cmd, ctrl) - logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") - if case let .response(_, json) = resp { - logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") - } - Task { - await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp) + 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, _ ctrl: chat_ctrl? = 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, ctrl)) + cont.resume(returning: chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log)) } } -func chatRecvMsg(_ ctrl: chat_ctrl? = nil) 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(ctrl) - cont.resume(returning: resp) - return resp + _ = withBGTask(bgDelay: msgDelay) { () -> APIResult? in + let evt: APIResult? = recvSimpleXMsg(ctrl) + cont.resume(returning: evt) + return evt } } } func apiGetActiveUser(ctrl: chat_ctrl? = nil) throws -> User? { - let r = chatSendCmdSync(.showActiveUser, ctrl) + 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, ctrl: chat_ctrl? = nil) throws -> User { - let r = chatSendCmdSync(.createActiveUser(profile: p, sameServers: sameServers, pastTimestamp: pastTimestamp), ctrl) +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] { @@ -147,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 { @@ -201,85 +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(ctrl: chat_ctrl? = nil) throws -> Bool { - let r = chatSendCmdSync(.startChat(mainApp: true), ctrl) + 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, ctrl: chat_ctrl? = nil) throws { - let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder), ctrl) +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, ctrl: chat_ctrl? = nil) throws { - let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder), ctrl) - 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 apiSaveAppSettings(settings: AppSettings) throws { - let r = chatSendCmdSync(.apiSaveSettings(settings: settings)) - if case .cmdOk = r { return } - throw r + try sendCommandOkRespSync(.apiSaveSettings(settings: settings)) } func apiGetAppSettings(settings: AppSettings) throws -> AppSettings { - let r = chatSendCmdSync(.apiGetSettings(settings: settings)) + let r: ChatResponse2 = try chatSendCmdSync(.apiGetSettings(settings: settings)) if case let .appSettings(settings) = r { return settings } - throw r + throw r.unexpected } -func apiExportArchive(config: ArchiveConfig) async throws { - try await sendCommandOkResp(.apiExportArchive(config: config)) +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 { @@ -290,8 +309,8 @@ 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) +func testStorageEncryption(key: String, ctrl: chat_ctrl? = nil) async throws { + try await sendCommandOkResp(.testStorageEncryption(key: key), ctrl: ctrl) } func apiGetChats() throws -> [ChatData] { @@ -304,56 +323,103 @@ 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 apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) async -> ChatItem? { - let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId) +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 apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? { - let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl) +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? { +private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> [ChatItem]? { let chatModel = ChatModel.shared - let r: ChatResponse + let r: APIResult if toChatType == .direct { var cItem: ChatItem? = nil let endTask = beginBGTask({ @@ -363,81 +429,118 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async } } }) - 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 } } -func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) async -> ChatItem? { - let r = await chatSendCmd(.apiCreateChatItem(noteFolderId: noteFolderId, file: file, msg: msg)) - if case let .newChatItem(_, aChatItem) = r { return aChatItem.chatItem } - createChatItemErrorAlert(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 } -private func sendMessageErrorAlert(_ r: ChatResponse) { +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))" ) } -private func createChatItemErrorAlert(_ r: ChatResponse) { - logger.error("apiCreateChatItem error: \(String(describing: r))") +private func createChatItemsErrorAlert(_ r: ChatError) { + logger.error("apiCreateChatItems error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error creating 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 +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 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 = chatSendCmdSync(.apiGetNtfToken) + let r: APIResult = chatApiSendCmdSync(.apiGetNtfToken) switch r { - case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer) - case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil) + 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))") return (nil, nil, .off, nil) @@ -445,9 +548,9 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String } 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) { @@ -459,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))") } @@ -467,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 { @@ -509,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 { @@ -519,28 +726,38 @@ func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws { try await sendCommandOkResp(.apiSetChatItemTTL(userId: userId, seconds: chatItemTTL.seconds)) } +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 getNetworkConfig() async throws -> NetCfg? { - let r = await chatSendCmd(.apiGetNetworkConfig) + let r: ChatResponse0 = try await chatSendCmd(.apiGetNetworkConfig) if case let .networkConfig(cfg) = r { return cfg } - throw r + throw r.unexpected } func setNetworkConfig(_ cfg: NetCfg, ctrl: chat_ctrl? = nil) throws { - let r = chatSendCmdSync(.apiSetNetworkConfig(networkConfig: cfg), ctrl) + 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 = chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo)) + let r: ChatResponse2 = try chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo)) if case .cmdOk = r { return } - throw r + 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)) } @@ -550,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)?, Alert?) { +func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactConnection)?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiAddContact: no current user") return (nil, nil) } - let r = await chatSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) - if case let .invitation(_, connReqInvitation, connection) = r { return ((connReqInvitation, connection), 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 @@ -658,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 { @@ -709,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))" ) } } @@ -725,29 +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 { +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 = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false) + 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( @@ -757,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 { @@ -775,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." @@ -877,20 +1225,24 @@ 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 { @@ -898,73 +1250,155 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws { } func uploadStandaloneFile(user: any UserLike, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (FileTransferMeta?, String?) { - let r = await chatSendCmd(.apiUploadStandaloneFile(userId: user.userId, file: file), ctrl) - if case let .sndStandaloneFileCreated(_, fileTransferMeta) = r { + let r: APIResult = await chatApiSendCmd(.apiUploadStandaloneFile(userId: user.userId, file: file), ctrl: ctrl) + if case let .result(.sndStandaloneFileCreated(_, fileTransferMeta)) = r { return (fileTransferMeta, nil) } else { - logger.error("uploadStandaloneFile error: \(String(describing: r))") - return (nil, String(describing: r)) + let err = responseError(r.unexpected) + logger.error("uploadStandaloneFile error: \(err)") + return (nil, err) } } func downloadStandaloneFile(user: any UserLike, url: String, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (RcvFileTransfer?, String?) { - let r = await chatSendCmd(.apiDownloadStandaloneFile(userId: user.userId, url: url, file: file), ctrl) - if case let .rcvStandaloneFileCreated(_, rcvFileTransfer) = r { + 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 { - logger.error("downloadStandaloneFile error: \(String(describing: r))") - return (nil, String(describing: r)) + 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 = await chatSendCmd(.apiStandaloneFileInfo(url: url), ctrl) - if case let .standaloneFileInfo(fileMeta) = r { + let r: APIResult = await chatApiSendCmd(.apiStandaloneFileInfo(url: url), ctrl: ctrl) + if case let .result(.standaloneFileInfo(fileMeta)) = r { return fileMeta } else { - logger.error("standaloneFileInfo error: \(String(describing: r))") + logger.error("standaloneFileInfo error: \(responseError(r.unexpected))") return nil } } -func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async { - if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) { - await chatItemSimpleUpdate(user, chatItem) - } +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 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." +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 ) - } - } else if let networkErrorAlert = networkErrorAlert(r) { - logger.error("apiReceiveFile network error: \(String(describing: r))") - am.showAlert(networkErrorAlert) - } else { - switch chatError(r) { - case .fileCancelled: - logger.debug("apiReceiveFile ignoring fileCancelled error") - case .fileAlreadyReceiving: - logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error") + ) + 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: - logger.error("apiReceiveFile error: \(String(describing: r))") - am.showAlertMsg( - title: "Error receiving file", - message: "Error: \(String(describing: r))" - ) + otherFileErrs.append(r) } } - return nil + + 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) @@ -973,12 +1407,12 @@ func cancelFile(user: User, fileId: Int64) async { } func apiCancelFile(fileId: Int64, ctrl: chat_ctrl? = nil) async -> AChatItem? { - let r = await chatSendCmd(.cancelFile(fileId: fileId), ctrl) + 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 } } @@ -988,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 { @@ -998,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 { @@ -1023,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 } } @@ -1043,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) + } + } + } } } @@ -1084,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 { @@ -1099,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 { @@ -1135,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, _ ctrl: chat_ctrl? = nil) async throws { - let r = await chatSendCmd(cmd, ctrl) +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 { @@ -1177,31 +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 apiBlockMemberForAll(_ groupId: Int64, _ memberId: Int64, _ blocked: Bool) async throws -> GroupMember { - let r = await chatSendCmd(.apiBlockMemberForAll(groupId: groupId, memberId: memberId, blocked: blocked), bgTask: false) - if case let .memberBlockedForAllUser(_, _, member, _) = r { return member } - throw r +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 { @@ -1214,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 [] } @@ -1229,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{ c in c.ready && c.active && !memberContactIds.contains(c.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 { @@ -1305,11 +1767,20 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni if encryptionStartedDefault.get() { encryptionStartedDefault.set(false) } - try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + 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) @@ -1354,31 +1825,34 @@ private func chatInitialized(start: Bool, refreshInvitations: Bool) throws { } } -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, 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 + } } } } @@ -1391,8 +1865,7 @@ 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 apiSetTempFolder(tempFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl) - try apiSetFilesFolder(filesFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl) + try apiSetAppFilePaths(filesFolder: getMigrationTempFilesDirectory().path, tempFolder: getMigrationTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path, ctrl: ctrl) _ = try apiStartChat(ctrl: ctrl) return migrationActiveUser } @@ -1438,24 +1911,37 @@ 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 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.chats = chats.map { Chat.init($0) } + m.updateChats(chats) + tm.activeFilter = nil + tm.userTags = tags + tm.updateChatTags(m.chats) } } else { await MainActor.run { m.userAddress = nil - m.chats = [] + m.updateChats([]) + tm.activeFilter = nil + tm.userTags = [] + tm.presetTags = [:] } } } @@ -1465,7 +1951,7 @@ class ChatReceiver { private var receiveMessages = true private var _lastMsgTime = Date.now - var messagesChannel: ((ChatResponse) -> Void)? = nil + var messagesChannel: ((APIResult) -> Void)? = nil static let shared = ChatReceiver() @@ -1483,7 +1969,12 @@ class ChatReceiver { while self.receiveMessages { if let msg = await chatRecvMsg() { self._lastMsgTime = .now - await processReceivedMsg(msg) + 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) } @@ -1500,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): @@ -1527,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 { @@ -1539,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) @@ -1571,7 +2073,7 @@ 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) } @@ -1579,59 +2081,71 @@ func processReceivedMsg(_ res: ChatResponse) async { case let .networkStatus(status, connections): // dispatch queue to synchronize access networkStatusesLock.sync { - var ns = m.networkStatuses + var ns = n.networkStatuses // slow loop is on the background thread for cId in connections { ns[cId] = status } // fast model update is on the main thread DispatchQueue.main.sync { - m.networkStatuses = ns + n.networkStatuses = ns } } case let .networkStatuses(_, statuses): () // dispatch queue to synchronize access networkStatusesLock.sync { - var ns = m.networkStatuses + var ns = n.networkStatuses // slow loop is on the background thread for s in statuses { ns[s.agentConnId] = s.networkStatus } // fast model update is on the main thread DispatchQueue.main.sync { - m.networkStatuses = ns + 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, 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): @@ -1642,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 { @@ -1678,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 { @@ -1686,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): @@ -1736,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): @@ -1767,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): @@ -1778,11 +2324,15 @@ func processReceivedMsg(_ res: ChatResponse) async { if let aChatItem = aChatItem { await chatItemSimpleUpdate(user, aChatItem) } - case let .rcvFileError(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, _): @@ -1799,11 +2349,15 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .sndFileCompleteXFTP(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) - case let .sndFileError(user, 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 @@ -1813,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) @@ -1849,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 { @@ -1887,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) { @@ -1921,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))") } @@ -1949,32 +2535,79 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async { } } -func refreshCallInvitations() throws { +func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemIDs: Set, _ member_: GroupMember?) async { 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) + 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 { + 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 justRefreshCallInvitations() throws -> [RcvCallInvitation] { +func refreshCallInvitations() async throws { 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 + 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() 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 4494adc0e8..92bcdcac53 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -36,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) diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 7d69466c07..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,9 +40,14 @@ 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() { // Present screen for continue migration if it wasn't finished yet @@ -57,6 +63,7 @@ struct SimpleXApp: App { } .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 @@ -80,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 } } } @@ -128,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) } } @@ -147,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 97415018bf..ab7a47b944 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -17,8 +17,8 @@ struct ActiveCallView: View { @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 @@ -27,24 +27,39 @@ struct ActiveCallView: View { var body: some View { ZStack(alignment: .topLeading) { ZStack(alignment: .bottom) { - if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil { + if let client = client, call.hasVideo { GeometryReader { g in let width = g.size.width * 0.3 ZStack(alignment: .topTrailing) { - CallViewRemote(client: client, activeCall: $activeCall, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, pipShown: $pipShown) - CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown) - .cornerRadius(10) - .frame(width: width, height: width / (localRendererAspectRatio ?? 1)) - .padding([.top, .trailing], 17) 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: 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, (!pipShown || !call.supportsVideo) { + if let call = m.activeCall, let client = client, (!pipShown || !call.hasVideo) { ActiveCallOverlay(call: call, client: client) } } @@ -54,6 +69,9 @@ struct ActiveCallView: View { .onAppear { logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)") AppDelegate.keepScreenOn(true) + Task { + await askRequiredPermissions() + } createWebRTCClient() dismissAllSheets() hideKeyboard() @@ -84,7 +102,7 @@ struct ActiveCallView: View { 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) } @@ -99,7 +117,7 @@ struct ActiveCallView: View { 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) @@ -110,7 +128,7 @@ struct ActiveCallView: View { call.callState = .invitationSent call.localCapabilities = capabilities } - if call.supportsVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() { + if call.hasVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() { try? AVAudioSession.sharedInstance().setCategory(.playback, options: [.allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) } CallSoundsPlayer.shared.startConnectingCallSound() @@ -120,7 +138,7 @@ struct ActiveCallView: View { 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))") } @@ -164,6 +182,9 @@ struct ActiveCallView: View { } if state.connectionState == "closed" { closeCallView(client) + if let callUUID = m.activeCall?.callUUID { + CallController.shared.endCall(callUUID: callUUID) + } m.activeCall = nil m.activeCallViewIsCollapsed = false } @@ -182,10 +203,18 @@ struct ActiveCallView: View { 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: @@ -214,16 +243,38 @@ struct ActiveCallView: View { ChatReceiver.shared.messagesChannel = nil return } - if case let .chatItemStatusUpdated(_, msg) = msg, - msg.chatInfo.id == call.contact.id, - case .sndCall = msg.chatItem.content, - case .sndRcvd = msg.chatItem.meta.itemStatus { + 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 @@ -239,44 +290,16 @@ struct ActiveCallOverlay: View { var body: some View { VStack { - switch call.localMedia { - case .video: + switch call.hasVideo { + case true: videoCallInfoView(call) .foregroundColor(.white) .opacity(0.8) - .padding() - - Spacer() - - HStack { - toggleAudioButton() - Spacer() - if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) { - toggleSpeakerButton() - .frame(width: 40, height: 40) - } else if call.hasMedia { - AudioDevicePicker() - .scaleEffect(2) - .frame(maxWidth: 40, maxHeight: 40) - } else { - Color.clear.frame(width: 40, height: 40) - } - Spacer() - endCallButton() - Spacer() - if call.videoEnabled { - flipCameraButton() - } else { - Color.clear.frame(width: 40, height: 40) - } - Spacer() - toggleVideoButton() - } - .padding(.horizontal, 20) - .padding(.bottom, 16) - .frame(maxWidth: .infinity, alignment: .center) - - case .audio: + .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 @@ -291,35 +314,32 @@ struct ActiveCallOverlay: View { } .foregroundColor(.white) .opacity(0.8) - .padding() + .padding(.horizontal) + .padding(.vertical, 10) .frame(maxHeight: .infinity) } - - Spacer() - - ZStack(alignment: .bottom) { - toggleAudioButton() - .frame(maxWidth: .infinity, alignment: .leading) - endCallButton() - // 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 - if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) { - toggleSpeakerButton() - .frame(maxWidth: .infinity, alignment: .trailing) - } else if call.hasMedia { - AudioDevicePicker() - .scaleEffect(2) - .frame(maxWidth: 50, maxHeight: 40) - .frame(maxWidth: .infinity, alignment: .trailing) - } else { - Color.clear.frame(width: 50, height: 40) - } - } - .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 { @@ -341,7 +361,7 @@ struct ActiveCallOverlay: View { HStack { Text(call.encryptionStatus) if let connInfo = call.connectionInfo { - Text("(") + Text(connInfo.text) + Text(")") + Text(verbatim: "(") + Text(connInfo.text) + Text(verbatim: ")") } } } @@ -370,7 +390,7 @@ struct ActiveCallOverlay: View { HStack { Text(call.encryptionStatus) if let connInfo = call.connectionInfo { - Text("(") + Text(connInfo.text) + Text(")") + Text(verbatim: "(") + Text(connInfo.text) + Text(verbatim: ")") } } } @@ -381,36 +401,54 @@ struct ActiveCallOverlay: View { 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 { - let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker - client.setSpeakerEnabledAndConfigureSession(!speakerEnabled) - DispatchQueue.main.async { - call.speakerEnabled = !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 @@ -418,53 +456,67 @@ struct ActiveCallOverlay: View { } } - 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 index 3d846c7b68..be41741ab5 100644 --- a/apps/ios/Shared/Views/Call/AudioDevicePicker.swift +++ b/apps/ios/Shared/Views/Call/AudioDevicePicker.swift @@ -2,7 +2,7 @@ // MPVolumeView.swift // SimpleX (iOS) // -// Created by Stanislav on 24.04.2024. +// Created by Avently on 24.04.2024. // Copyright © 2024 SimpleX Chat. All rights reserved. // diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index 64b565e8e6..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,8 +121,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession) RTCAudioSession.sharedInstance().isAudioEnabled = true do { - let supportsVideo = ChatModel.shared.activeCall?.supportsVideo == true - if supportsVideo { + 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]) @@ -115,7 +133,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse try? await Task.sleep(nanoseconds: UInt64(i) * 300_000000) if let preferred = audioSession.preferredInputDevice() { await MainActor.run { try? audioSession.setPreferredInput(preferred) } - } else if supportsVideo { + } else if hasVideo { await MainActor.run { try? audioSession.overrideOutputAudioPort(.speaker) } } } @@ -156,6 +174,19 @@ 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)") @@ -171,32 +202,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse self.reportExpiredCall(payload: payload, completion) return } - 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)") - self.reportExpiredCall(payload: payload, completion) - return - } - } - logger.debug("CallController: initialized chat") - startChatForCall() - logger.debug("CallController: started chat") - self.shouldSuspendChat = true - // There are no invitations in the model, as it was processed by NSE - _ = try? justRefreshCallInvitations() - logger.debug("CallController: updated call invitations chat") - // 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 { + 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") - let update = self.cxCallUpdate(invitation: invitation) self.provider.reportNewIncomingCall(with: uuid, update: update) { error in if error != nil { m.callInvitations.removeValue(forKey: contactId) @@ -205,11 +223,31 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse completion() } } else { + 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, @@ -239,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))") - 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) @@ -261,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() { @@ -272,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 @@ -288,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) @@ -307,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) } @@ -328,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 { diff --git a/apps/ios/Shared/Views/Call/CallManager.swift b/apps/ios/Shared/Views/Call/CallManager.swift index a6d5ea17c4..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) } @@ -126,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 a3201d9351..e779093a24 100644 --- a/apps/ios/Shared/Views/Call/CallViewRenderers.swift +++ b/apps/ios/Shared/Views/Call/CallViewRenderers.swift @@ -10,40 +10,49 @@ import AVKit struct CallViewRemote: UIViewRepresentable { var client: WebRTCClient - var activeCall: Binding + @ObservedObject var call: Call @State var enablePip: (Bool) -> Void = {_ in } @Binding var activeCallViewIsCollapsed: Bool + @Binding var contentMode: UIView.ContentMode @Binding var pipShown: Bool - init(client: WebRTCClient, activeCall: Binding, activeCallViewIsCollapsed: Binding, pipShown: Binding) { - self.client = client - self.activeCall = activeCall - self._activeCallViewIsCollapsed = activeCallViewIsCollapsed - self._pipShown = pipShown - } - 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 - if AVPictureInPictureController.isPictureInPictureSupported() { - makeViewWithRTCRenderer(call, remoteRenderer, view, context) - } + 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(_ call: WebRTCClient.Call, _ remoteRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) { - let pipRemoteRenderer = RTCMTLVideoView(frame: view.frame) - pipRemoteRenderer.videoContentMode = .scaleAspectFill - + 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) - addSubviewAndResize(pipRemoteRenderer, into: pipVideoCallViewController.view) let pipContentSource = AVPictureInPictureController.ContentSource( activeVideoCallSourceView: view, contentViewController: pipVideoCallViewController @@ -55,7 +64,9 @@ struct CallViewRemote: UIViewRepresentable { context.coordinator.pipController = pipController context.coordinator.willShowHide = { show in if show { - client.addRemoteRenderer(call, pipRemoteRenderer) + client.addRemoteCameraRenderer(pipRemoteCameraRenderer) + client.addRemoteScreenRenderer(pipRemoteScreenRenderer) + context.coordinator.relayout() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { activeCallViewIsCollapsed = true } @@ -67,13 +78,29 @@ struct CallViewRemote: UIViewRepresentable { } context.coordinator.didShowHide = { show in if show { - remoteRenderer.isHidden = true + remoteCameraRenderer.isHidden = true + remoteScreenRenderer.isHidden = true } else { - client.removeRemoteRenderer(call, pipRemoteRenderer) - remoteRenderer.isHidden = false + 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 */ { @@ -88,24 +115,50 @@ struct CallViewRemote: UIViewRepresentable { } func makeCoordinator() -> Coordinator { - 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) } @@ -127,11 +180,20 @@ struct CallViewRemote: UIViewRepresentable { } 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) + } } } @@ -148,51 +210,109 @@ struct CallViewRemote: UIViewRepresentable { 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, pipShown: 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) - DispatchQueue.main.async { - pipStateChanged = { shown in - localRenderer.isHidden = shown - } + 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 4647995b28..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 { @@ -37,13 +38,14 @@ struct IncomingCallView: View { } 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 } @@ -63,7 +65,7 @@ struct IncomingCallView: View { .padding(.horizontal, 16) .padding(.vertical, 12) .frame(maxWidth: .infinity) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) + .modifier(ThemedBackground()) .onAppear { dismissAllSheets() } } @@ -76,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/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift index 333dc082d5..ef9135761c 100644 --- a/apps/ios/Shared/Views/Call/WebRTC.swift +++ b/apps/ios/Shared/Views/Call/WebRTC.swift @@ -18,49 +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 supportsVideo: Bool { get { peerMedia == .video || localMedia == .video } } + var hasVideo: Bool { get { localMediaSources.hasVideo || peerMediaSources.hasVideo } } } enum CallDirection { @@ -105,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 @@ -167,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) @@ -205,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: @@ -224,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) @@ -238,6 +249,8 @@ enum WCallResponse: Equatable, Decodable { case state case connectionInfo case message + case source + case enabled } var respType: String { @@ -249,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" @@ -283,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": @@ -324,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: @@ -376,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 } } } diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift index 0b4917c103..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 { @@ -51,17 +60,20 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg private let rtcAudioSession = RTCAudioSession.sharedInstance() 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() @@ -78,39 +90,45 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg 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,18 +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 removeRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) { - activeCall.remoteStream?.remove(renderer) + func removeRemoteCameraRenderer(_ renderer: RTCVideoRenderer) { + if activeCall?.remoteVideoTrack != nil { + activeCall?.remoteVideoTrack?.remove(renderer) + } else { + cameraRenderers.removeAll(where: { $0.isEqual(renderer) }) + } } - func startCaptureLocalVideo(_ activeCall: Call) { + 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 @@ -348,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 } @@ -377,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) @@ -397,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) @@ -407,19 +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.localCamera as? RTCCameraVideoCapturer)?.stopCapture() 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 { @@ -428,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 } } @@ -489,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() @@ -514,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: () } } } @@ -537,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)) } } @@ -592,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 } @@ -609,7 +895,7 @@ extension WebRTCClient { 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, let preferred = self.rtcAudioSession.session.preferredInputDevice() { + if hasExternalAudioDevice && !skipExternalDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() { try self.rtcAudioSession.setPreferredInput(preferred) } else { try self.rtcAudioSession.overrideOutputAudioPort(.speaker) @@ -619,7 +905,7 @@ extension WebRTCClient { try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue) try self.rtcAudioSession.overrideOutputAudioPort(.none) } - if hasExternalAudioDevice { + if hasExternalAudioDevice && !skipExternalDevice { logger.debug("WebRTCClient: configuring session with external device available, skip configuring speaker") } try self.rtcAudioSession.setActive(true) @@ -650,25 +936,59 @@ 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: () + let device = activeCall?.device ?? notConnectedCall?.device + if activeCall != nil { + activeCall?.device = device == .front ? .back : .front + } else { + notConnectedCall?.device = device == .front ? .back : .front } - if let call = activeCall.wrappedValue { - startCaptureLocalVideo(call) - } - } - - private func setTrackEnabled(_ type: T.Type, _ enabled: Bool) { - activeCall.wrappedValue?.connection.transceivers - .compactMap { $0.sender.track as? T } - .forEach { $0.isEnabled = enabled } + startCaptureLocalVideo( + activeCall?.device ?? notConnectedCall?.device, + (activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCCameraVideoCapturer + ) } } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 140b609902..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 @@ -26,29 +25,29 @@ struct ChatInfoToolbar: View { ChatInfoImage( chat: chat, size: imageSize, - color: colorScheme == .dark - ? chatImageColorDark - : chatImageColorLight + color: Color(uiColor: .tertiaryLabel) ) .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 55e84f20d3..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,115 +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 conn = contact.activeConn { + Section { - infoRow(Text(String("E2E encryption")), conn.connPQEnabled ? "Quantum resistant" : "Standard") - } - } + if let code = connectionCode { verifyCodeButton(code) } + contactPreferencesButton() + sendReceiptsOption() + if let connStats = connectionStats, + connStats.ratchetSyncAllowed { + synchronizeConnectionButton() + } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } - if let contactLink = contact.contactLink { - Section { - SimpleXLinkQRCode(uri: contactLink) - Button { - showShareSheet(items: [simplexChatLink(contactLink)]) + NavigationLink { + ChatWallpaperEditorSheet(chat: chat) } label: { - Label("Share address", systemImage: "square.and.arrow.up") + Label("Chat theme", systemImage: "photo") } - } 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 + .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 { @@ -235,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) { @@ -242,44 +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, size: 192, color: Color(uiColor: .tertiarySystemFill)) - .padding(.top, 12) - .padding() + .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) ) @@ -302,7 +394,7 @@ struct ChatInfoView: View { } .frame(maxWidth: .infinity, alignment: .center) } - + private func localAliasTextEdit() -> some View { TextField("Set contact name…", text: $localAlias) .disableAutocorrection(true) @@ -317,9 +409,9 @@ struct ChatInfoView: View { setContactAlias() } .multilineTextAlignment(.center) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } - + private func setContactAlias() { Task { do { @@ -334,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( @@ -355,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", @@ -362,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 @@ -390,7 +504,7 @@ struct ChatInfoView: View { setSendReceipts() } } - + private func setSendReceipts() { var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults chatSettings.sendRcpts = sendReceipts.bool() @@ -399,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 @@ -414,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 @@ -452,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?"), @@ -485,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 { @@ -511,7 +618,7 @@ struct ChatInfoView: View { } } } - + private func abortSwitchContactAddress() { Task { do { @@ -529,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 + } + } } } @@ -577,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 e3913431f5..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,7 +36,7 @@ struct CICallItemView: View { case .error: missedCallIcon(sent).foregroundColor(.orange) } - CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false) .padding(.bottom, 8) .padding(.horizontal, 12) } @@ -49,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 5c9ea0f6d8..02be8af73b 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -11,9 +11,11 @@ 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 @@ -52,8 +54,8 @@ struct CIChatFeatureView: View { 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) @@ -66,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, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param) - case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).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 } } @@ -81,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 @@ -93,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) @@ -104,6 +106,9 @@ struct CIChatFeatureView: View { struct CIChatFeatureView_Previews: PreviewProvider { static var previews: some View { let enabled = FeatureEnabled(forUser: false, forContact: false) - CIChatFeatureView(chat: Chat.sampleData, 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/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 ae9e09b138..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,24 +49,29 @@ 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 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 } } @@ -73,10 +82,10 @@ struct CIFileView: View { logger.debug("CIFileView fileAction") if let file = file { switch (file.fileStatus) { - case .rcvInvitation: + 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 { await receiveFile(user: user, fileId: file.fileId) } @@ -107,11 +116,28 @@ struct CIFileView: View { 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 } } @@ -135,9 +161,10 @@ struct CIFileView: View { 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(file) { - fileIcon("arrow.down.doc.fill", color: .accentColor) + 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) } } } @@ -225,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( @@ -242,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 1c9df5fcbf..3fcf578875 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -11,7 +11,8 @@ 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 @@ -21,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) { @@ -40,18 +43,18 @@ struct CIGroupInvitationView: View { .overlay(DetermineWidth()) ( Text(chatIncognito ? "Tap to join incognito" : "Tap to join") - .foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor) + .foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary) .font(.callout) - + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + + Text(verbatim: " ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) .overlay(DetermineWidth()) } } else { ( groupInvitationText() - + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + + Text(verbatim: " ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) .overlay(DetermineWidth()) } @@ -63,12 +66,11 @@ struct CIGroupInvitationView: View { } } - CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false) + 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 @@ -82,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 @@ -97,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) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 16974147c8..d30369339d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -11,34 +11,44 @@ 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) + } + .if(!smallView) { view in + view.modifier(PrivacyBlur(blurred: $blurred)) + } + .if(!blurred) { v in + v.simultaneousGesture(TapGesture().onEnded { showFullScreenImage = true }) } - .onTapGesture { showFullScreenImage = true } .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenImage = false } - } else if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - imageView(uiImage) - .onTapGesture { + } 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) @@ -61,17 +71,27 @@ struct CIImageView: View { 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 : maxWidth - DispatchQueue.main.async { imgWidth = w } return ZStack(alignment: .topTrailing) { if img.imageData == nil { Image(uiImage: img) @@ -83,7 +103,26 @@ struct CIImageView: View { .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() + } } } @@ -100,13 +139,16 @@ struct CIImageView: View { 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() } } } @@ -114,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) } @@ -127,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 17b93930fe..fc73778239 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -11,110 +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, showStatus: showStatus, showEdited: showEdited) - case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) + 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, showStatus: showStatus, showEdited: showEdited) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) - } - case .partial: - ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) - } - } - default: - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited) } } } } -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, + 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 + showEdited: Bool = true, + showViaProxy: Bool, + showTimesamp: Bool ) -> Text { var r = Text("") + 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", color) + 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 showViaProxy, meta.sentViaProxy == true { + appendSpace() + r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2) } if showStatus { - 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 + 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 + Text(" ") + r = r + colored(Text(image), metaColor) } else if !meta.disappearing { - r = r + statusIconText("circlebadge.fill", .clear) + Text(" ") + r = r + colorMode.statusSpacer(meta.itemStatus.sent) } + 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 b4b190a43a..eacbe9360a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -13,95 +13,136 @@ 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 urlDecrypted: URL? @State private var decryptionInProgress: Bool = false - @State private var showFullScreenPlayer = 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) { - let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet() - self._urlDecrypted = State(initialValue: decrypted) - if let decrypted = decrypted { - self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(decrypted, false)) - self._fullPlayer = State(initialValue: AVPlayer(url: decrypted)) - } - 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 decrypted = urlDecrypted { - videoView(player, decrypted, file, preview, duration) - } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil { - videoViewEncrypted(file, defaultPreview, 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, 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: () - } + 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 let file = file, case .rcvInvitation = file.fileStatus { - Button { - receiveFileIfValidSize(file: file, receiveFile: receiveFile) - } label: { - playPauseIcon("play.fill") + if !smallView { + durationProgress() } } + 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 } } @@ -110,33 +151,29 @@ struct CIVideoView: View { ZStack(alignment: .center) { let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) imageView(defaultPreview) - .fullScreenCover(isPresented: $showFullScreenPlayer) { - if let decrypted = urlDecrypted { - fullScreenPlayer(decrypted) - } - } - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { decrypt(file: file) { showFullScreenPlayer = urlDecrypted != nil } - } + }) .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenPlayer = false } - if !decryptionInProgress { - Button { - decrypt(file: file) { - if urlDecrypted != nil { - videoPlaying = true - player?.play() - } - } - } label: { + 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() } - .disabled(!canBePlayed) - } else { - videoDecryptionProgress() } } } @@ -144,7 +181,6 @@ 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 || (file.fileStatus == .sndStored && file.fileProtocol == .local) @@ -156,35 +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: () - } + }) } .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenPlayer = false } - if !videoPlaying { - Button { - m.stopPreviousRecPlay = url - player.play() - } label: { - playPauseIcon(canBePlayed ? "play.fill" : "play.slash") - } - .disabled(!canBePlayed) + 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) @@ -196,14 +231,53 @@ struct CIVideoView: View { } } + 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: 12, height: 12) + .frame(width: smallView ? 12 * sizeMultiplier * 1.6 : 12, height: smallView ? 12 * sizeMultiplier * 1.6 : 12) .foregroundColor(color) - .padding(.leading, 4) - .frame(width: 40, height: 40) + .padding(.leading, smallView ? 0 : 4) + .frame(width: 40 * sizeMultiplier, height: 40 * sizeMultiplier) .background(Color.black.opacity(0.35)) .clipShape(Circle()) } @@ -211,50 +285,57 @@ struct CIVideoView: View { private func videoDecryptionProgress(_ color: Color = .white) -> some View { ProgressView() .progressViewStyle(.circular) - .frame(width: 12, height: 12) + .frame(width: smallView ? 12 * sizeMultiplier : 12, height: smallView ? 12 * sizeMultiplier : 12) .tint(color) - .frame(width: 40, height: 40) + .frame(width: smallView ? 40 * sizeMultiplier * 0.9 : 40, height: smallView ? 40 * sizeMultiplier * 0.9 : 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 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 : maxWidth - DispatchQueue.main.async { videoWidth = w } return ZStack(alignment: .topTrailing) { Image(uiImage: img) .resizable() .scaledToFit() .frame(width: w) - loadingIndicator() + .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: @@ -271,7 +352,16 @@ struct CIVideoView: View { } 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): @@ -280,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() } } } @@ -291,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 { @@ -302,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, receiveFile: @escaping (User, Int64, 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, false) + await receiveFile(user, file.fileId, false, false) } } } @@ -331,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() @@ -354,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 @@ -391,10 +490,12 @@ struct CIVideoView: View { urlDecrypted = await file.fileSource?.decryptedGetOrCreate(&ChatModel.shared.filesToDelete) await MainActor.run { if let decrypted = urlDecrypted { - player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + if !smallView { + player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + } fullPlayer = AVPlayer(url: decrypted) } - decryptionInProgress = true + decryptionInProgress = false completed?() } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 3aecb65ebd..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) + 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 95c3347f90..b27d266d8a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -9,38 +9,36 @@ import SwiftUI import SimpleXChat -let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21) -let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19) -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 - @State private var showSecrets = false - @State private var showQuoteSecrets = false - - @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()) @@ -57,42 +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 } } @@ -109,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) @@ -157,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, pad: Bool = false) -> 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, 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 { @@ -192,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) @@ -208,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) @@ -235,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 { @@ -250,7 +258,7 @@ struct FramedItemView: View { VStack(alignment: .leading, spacing: 2) { Text(sender) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(qi.chatDir == .groupSnd ? .accentColor : theme.colors.secondary) .lineLimit(1) ciQuotedMsgTextView(qi, lines: 2) } @@ -262,14 +270,12 @@ struct FramedItemView: View { .padding(.top, 6) .padding(.horizontal, 12) } - + + @inline(__always) private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View { - toggleSecrets(qi.formattedText, $showQuoteSecrets, - MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, showSecrets: showQuoteSecrets) - .lineLimit(lines) - .font(.subheadline) - .padding(.bottom, 6) - ) + MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, textStyle: .subheadline) + .lineLimit(lines) + .padding(.bottom, 6) } private func ciQuoteIconView(_ image: String) -> some View { @@ -289,24 +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 = toggleSecrets(ft, $showSecrets, MsgContentView( + let v = MsgContentView( chat: chat, text: text, formattedText: ft, + textStyle: .body, meta: ci.meta, + mentions: ci.mentions, + userMemberId: chat.chatInfo.groupInfo?.membership.memberId, rightToLeft: rtl, - showSecrets: showSecrets - )) + 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) @@ -332,13 +341,12 @@ struct FramedItemView: View { return videoWidth } } -} -@ViewBuilder func toggleSecrets(_ ft: [FormattedText]?, _ showSecrets: Binding, _ v: V) -> some View { - if let ft = ft, ft.contains(where: { $0.isSecret }) { - v.onTapGesture { showSecrets.wrappedValue.toggle() } - } else { - v + private func showQuotedItemDoesNotExistAlert() { + AlertManager.shared.showAlertMsg( + title: "No message", + message: "This message was deleted or not received yet." + ) } } @@ -349,13 +357,6 @@ 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 && ci.meta.itemForwarded == nil && text == "" @@ -365,35 +366,35 @@ func onlyImageOrVideo(_ ci: ChatItem) -> Bool { 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)) } @@ -402,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)) } } @@ -420,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 cb0b61f537..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) } @@ -36,8 +35,8 @@ struct MarkedDeletedItemView: View { 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 { @@ -68,11 +67,15 @@ 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" - case .blockedByAdmin: "blocked by admin" - case .deleted, nil: "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" + } } } } @@ -80,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 ccd7ac0a12..e04584dfff 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -11,46 +11,74 @@ import SimpleXChat 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 showSecrets: Bool + 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() @@ -60,95 +88,276 @@ struct MsgContentView: View { private func stopTyping() { timer?.invalidate() timer = nil + typingIdx = 0 } - private func msgContentView() -> Text { - var v = messageText(text, formattedText, sender, showSecrets: showSecrets) + @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, showSecrets: Bool) -> Text { - let s = text - var res: Text - if let ft = formattedText, ft.count > 0 && ft.count <= 200 { - res = formatText(ft[0], preview, showSecret: showSecrets) - var i = 1 - while i < ft.count { - res = res + formatText(ft[i], preview, showSecret: showSecrets) - 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, showSecret: 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 - showSecret - ? Text(t) - : Text(AttributedString(t, attributes: AttributeContainer([ - .foregroundColor: UIColor.clear as Any, - .backgroundColor: UIColor.secondarySystemFill as Any - ]))) - 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 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 { @@ -162,9 +371,9 @@ struct MsgContentView_Previews: PreviewProvider { chat: Chat.sampleData, text: chatItem.text, formattedText: chatItem.formattedText, + textStyle: .body, sender: chatItem.memberDisplayName, - meta: chatItem.meta, - showSecrets: false + meta: chatItem.meta ) .environmentObject(Chat.sampleData) } diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index 7237711a2a..dfc620c402 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -11,14 +11,16 @@ import SimpleXChat struct ChatItemForwardingView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss - var ci: ChatItem + var chatItems: [ChatItem] var fromChatInfo: ChatInfo @Binding var composeState: ComposeState @State private var searchText: String = "" - @FocusState private var searchFocused + @State private var alert: SomeAlert? + private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats) var body: some View { NavigationView { @@ -35,98 +37,76 @@ struct ChatItemForwardingView: View { } } } + .modifier(ThemedBackground()) + .alert(item: $alert) { $0.alert } } - @ViewBuilder private func forwardListView() -> some View { + private func forwardListView() -> some View { VStack(alignment: .leading) { - let chatsToForwardTo = filterChatsToForwardTo() if !chatsToForwardTo.isEmpty { - ScrollView { - LazyVStack(alignment: .leading, spacing: 8) { - searchFieldView(text: $searchText, focussed: $searchFocused) - .padding(.leading, 2) - let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase - let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) } - ForEach(chats) { chat in - Divider() - forwardListNavLinkView(chat) - .disabled(chatModel.deletedChats.contains(chat.chatInfo.id)) - } + 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)) } - .padding(.horizontal) - .padding(.vertical, 8) - .background(Color(uiColor: .systemBackground)) - .cornerRadius(12) - .padding(.horizontal) } - .background(Color(.systemGroupedBackground)) + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) + .modifier(ThemedBackground(grouped: true)) } else { - emptyList() + ZStack { + emptyList() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .modifier(ThemedBackground()) } } } - private func filterChatsToForwardTo() -> [Chat] { - var filteredChats = chatModel.chats.filter({ canForwardToChat($0) }) - if let index = filteredChats.firstIndex(where: { $0.chatInfo.chatType == .local }) { - let privateNotes = filteredChats.remove(at: index) - filteredChats.insert(privateNotes, at: 0) - } - return filteredChats - } - - private func filterChatSearched(_ chat: Chat, _ 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(_ chat: Chat) -> Bool { - switch chat.chatInfo { - 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 - } - } - private func emptyList() -> some View { Text("No filtered chats") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) } - @ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View { + @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 { - dismiss() - if chat.id == fromChatInfo.id { - composeState = ComposeState( - message: composeState.message, - preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, - contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo) - ) + if prohibited { + alert = SomeAlert( + alert: mkAlert( + title: "Cannot forward message", + message: "Selected chat preferences prohibit this message." + ), + id: "forward prohibited by preferences" + ) } else { - composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo) - chatModel.chatId = chat.id + 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(.primary) + .foregroundColor(prohibited ? theme.colors.secondary : theme.colors.onBackground) .lineLimit(1) if chat.chatInfo.incognito { Spacer() @@ -134,7 +114,7 @@ struct ChatItemForwardingView: View { .resizable() .scaledToFit() .frame(width: 22, height: 22) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -144,8 +124,9 @@ struct ChatItemForwardingView: View { #Preview { ChatItemForwardingView( - ci: ChatItem.getSample(1, .directSnd, .now, "hello"), + 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 19aa261396..cd75d1b0cd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -12,11 +12,14 @@ import SimpleXChat struct ChatItemInfoView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss - @Environment(\.colorScheme) var colorScheme + @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 { @@ -99,12 +102,14 @@ 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) @@ -112,6 +117,7 @@ struct ChatItemInfoView: View { Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward") } .tag(CIInfoTab.forwarded) + .modifier(ThemedBackground()) } } .onAppear { @@ -121,12 +127,13 @@ 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() @@ -157,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 { @@ -181,7 +217,7 @@ struct ChatItemInfoView: View { } else { Text("No history") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) } } @@ -191,13 +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) + 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 { @@ -221,28 +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 != "" { - TextBubble(text: text, formattedText: formattedText, sender: 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) } } private struct TextBubble: View { + @EnvironmentObject var theme: AppTheme var text: String var formattedText: [FormattedText]? var sender: String? = nil - @State private var showSecrets = false + var mentions: [String: CIMention]? + var userMemberId: String? + var backgroundColor: UIColor + @State private var showSecrets: Set = [] var body: some View { - toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, showSecrets: showSecrets)) + let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: backgroundColor) + return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) } } - @ViewBuilder private func quoteTab(_ qi: CIQuote) -> some View { + private func quoteTab(_ qi: CIQuote) -> some View { GeometryReader { g in let maxWidth = (g.size.width - 32) * 0.84 ScrollView { @@ -260,13 +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)) + 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 { @@ -289,13 +332,13 @@ 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 forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View { + private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View { ScrollView { VStack(alignment: .leading, spacing: 16) { details() @@ -315,8 +358,9 @@ struct ChatItemInfoView: View { Button { Task { await MainActor.run { - chatModel.chatId = forwardedFromItem.chatInfo.id - dismiss() + ItemsModel.shared.loadOpenChat(forwardedFromItem.chatInfo.id) { + dismiss() + } } } } label: { @@ -327,12 +371,12 @@ struct ChatItemInfoView: View { Divider().padding(.top, 32) Text("Recipient(s) can't see who this message is from.") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } - @ViewBuilder private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View { + private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View { HStack { ChatInfoImage(chat: Chat(chatInfo: forwardedFromItem.chatInfo), size: 48) .padding(.trailing, 6) @@ -341,29 +385,29 @@ struct ChatItemInfoView: View { VStack(alignment: .leading) { Text("you") .italic() - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) Text(forwardedFromItem.chatInfo.chatViewName) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .lineLimit(1) } } else if case let .groupRcv(groupMember) = forwardedFromItem.chatItem.chatDir { VStack(alignment: .leading) { Text(groupMember.chatViewName) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .lineLimit(1) Text(forwardedFromItem.chatInfo.chatViewName) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .lineLimit(1) } } else { Text(forwardedFromItem.chatInfo.chatViewName) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .lineLimit(1) } } } - @ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { ScrollView { VStack(alignment: .leading, spacing: 16) { details() @@ -378,56 +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, size: 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 { @@ -469,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")] @@ -520,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 d580fb5f3e..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, - allowMenu: Binding = .constant(false), - audioPlayer: Binding = .constant(nil), - playbackState: Binding = .constant(.noPlayback), - playbackTime: Binding = .constant(nil) + allowMenu: Binding = .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) + 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: 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,14 +144,14 @@ struct ChatItemContentView: 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(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary) - case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor) - case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).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() @@ -131,25 +178,25 @@ struct ChatItemContentView: View { } 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(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor) + CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor) } private var mergedGroupEventText: Text? { @@ -169,7 +216,7 @@ struct ChatItemContentView: 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) } @@ -179,7 +226,7 @@ struct ChatItemContentView: View { 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(.secondary) + .foregroundColor(theme.colors.secondary) .fontWeight(.light) : e2eeInfoNoPQText() } @@ -187,39 +234,40 @@ struct ChatItemContentView: View { 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(.secondary) + .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) } @@ -238,7 +286,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) ChatItemView( chat: Chat.sampleData, @@ -249,7 +297,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) ChatItemView( chat: Chat.sampleData, @@ -260,7 +308,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) ChatItemView( chat: Chat.sampleData, @@ -271,7 +319,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) ChatItemView( chat: Chat.sampleData, @@ -282,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 = { 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 + let newSplits: [Int64] + + init(oldUnreadSplitIndex: Int, newUnreadSplitIndex: Int, trimmedIds: Set, newSplits: [Int64]) { + self.oldUnreadSplitIndex = oldUnreadSplitIndex + self.newUnreadSplitIndex = newUnreadSplitIndex + self.trimmedIds = trimmedIds + self.newSplits = newSplits + } +} + +private func removeDuplicatesAndModifySplitsOnBeforePagination( + _ unreadAfterItemId: Int64, + _ newItems: inout [ChatItem], + _ newIds: Set, + _ splits: [Int64], + _ visibleItemIndexes: ClosedRange +) -> 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() + 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, + _ chat: Chat, + _ splits: [Int64] +) -> ([Int64], Int) { + var unreadInLoaded = unreadInLoaded + var firstItemIdBelowAllSplits: Int64? = nil + var splitsToRemove: Set = [] + 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 +) 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>] = [] + 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, Int) { + var unreadInLoaded = 0 + var ids: Set = 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) -> (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 + + 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, _ 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() + 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>? = 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>, + mergeCategory: CIMergeCategory?, + unreadIds: BoxedValue>, + startIndexInReversedItems: Int, + hash: String + ) + + func revealItems(_ reveal: Bool, _ revealedItems: Binding>) { + 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 + /** range of indexes inside LazyColumn where the first element is the split (it's index is [indexRangeInParentItems.first]) */ + let indexRangeInParentItems: ClosedRange +} + +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.. fromIndex + ? -nonReversedItems[fromIndex + 1..?, _ 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: Equatable, Hashable { + static func == (lhs: BoxedValue, rhs: BoxedValue) -> 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.ListState, _ mergedItems: MergedItems) -> ClosedRange { + 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, loadingBottomItems: Binding, _ 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, + _ ignoreLoadingRequests: Binding, + _ listState: EndlessScrollView.ListState, + _ mergedItems: BoxedValue, + 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.ListState, + _ ignoreLoadingRequests: Binding, + _ 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.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( + _ 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 4055ca2b28..c136ebc01b 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -9,44 +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 = BoxedValue(MergedItems.create(ItemsModel.shared.reversedChatItems, [], ItemsModel.shared.chatState)) + @State var revealedItems: Set = 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 = [] - @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? = 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 = 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 } @@ -54,97 +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 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( @@ -153,7 +339,9 @@ struct ChatView: View { chat.chatInfo = .group(groupInfo: gInfo) chat.created = Date.now } - ) + ), + onSearch: { focusSearch() }, + localAlias: groupInfo.localAlias ) } } else if case .local = cInfo { @@ -161,82 +349,78 @@ struct ChatView: View { } } ToolbarItem(placement: .navigationBarTrailing) { - 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) - } + if selectedChatItems != nil { + Button { + withAnimation { + selectedChatItems = nil } - Menu { - if callsPrefEnabled && chatModel.activeCall == nil { - 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: 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: 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() } - case .local: - searchButton() - 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 // This check prevents the call to apiContactInfo after the app is suspended, and the database is closed. @@ -264,6 +448,40 @@ 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 { @@ -272,7 +490,7 @@ struct ChatView: View { Image(systemName: "magnifyingglass") TextField("Search", text: $searchText) .focused($searchFocussed) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .frame(maxWidth: .infinity) Button { @@ -282,151 +500,399 @@ struct ChatView: View { } } .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) - .foregroundColor(.secondary) + .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.meta.itemForwarded == nil } + private func filtered(_ reversedChatItems: Array) -> Array { + 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) { chatId in - if let 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.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 + 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.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: @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: @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) @@ -437,8 +903,8 @@ struct ChatView: View { private func endCallButton(_ call: Call) -> some View { Button { - if let uuid = call.callkitUUID { - CallController.shared.endCall(callUUID: uuid) + if CallController.useCallKit(), let callUUID = call.callUUID { + CallController.shared.endCall(callUUID: callUUID) } else { CallController.shared.endCall(call: call) {} } @@ -449,18 +915,40 @@ struct ChatView: View { 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") @@ -486,149 +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, - itemWidth: 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 - @State var itemWidth: 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 @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? = nil + @State private var showArchivingReports = false @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? - @State private var showForwardingSheet: Bool = false + @State private var msgWidth: CGFloat = 0 + @State private var touchInProgress: Bool = false + + @Binding var selectedChatItems: Set? + @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? = 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? = 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 { - // Switch branches just to work around context menu problem when 'revealed' changes but size of item isn't - if revealed { - chatItemView(chatItem, range, prevItem) - } 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) -> ([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?, _ 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?, + _ 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) - .lineLimit(2) - .padding(.leading, memberImageSize + 14) - .padding(.top, 7) - } - HStack(alignment: .top, spacing: 8) { - ProfileImage(imageStr: member.memberProfile.image, size: 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) } } @@ -643,27 +1537,29 @@ struct ChatView: View { } } - @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat) -> some View { + func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading - let uiMenu: Binding = 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(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, 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) @@ -671,41 +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.deletable && !di.localNote { - 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) - } - .sheet(isPresented: $showForwardingSheet) { - if #available(iOS 16.0, *) { - ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState) - .presentationDetents([.fraction(0.8)]) - } else { - ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState) - } + ChatItemInfoView(ci: ci, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, chatItemInfo: $chatItemInfo) } } @@ -729,166 +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?, live: Bool) -> [UIMenuElement] { - var menu: [UIMenuElement] = [] - if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed { - let rs = allReactions(ci) + @ViewBuilder + private func menu(_ ci: ChatItem, _ range: ClosedRange?, 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) + } + 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, - 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) + availableReactions.count > 0 { + reactionsGroup } if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote { - menu.append(replyUIAction(ci)) + 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) { - menu.append(downloadFileAction(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 { - menu.append(forwardUIAction(ci)) + forwardButton } if !ci.isLiveDummy { - menu.append(viewInfoUIAction(ci)) + viewInfoButton(ci) } if revealed { - menu.append(hideUIAction()) + hideButton() } if ci.meta.itemDeleted == nil && !ci.localNote, let file = ci.file, let cancelAction = file.cancelAction { - menu.append(cancelFileUIAction(file.fileId, 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()) - menu.append(deleteUIAction(ci)) + if revealed { shrinkButton() } else { expandButton() } + deleteButton(ci) } else if ci.showLocalDelete { - menu.append(deleteUIAction(ci)) + 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 { + 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 { + 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 forwardUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Forward", comment: "chat item action"), - image: UIImage(systemName: "arrowshape.turn.up.forward") - ) { _ in - showForwardingSheet = true + 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 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) } - } - } - - private func topReactionsCount(_ rs: [UIAction]) -> Int { - rs.count > 4 ? 3 : 4 + /// Reactions, which has not been used yet + private var availableReactions: Array { + MsgReaction.values + .filter { reaction in + !chatItem.reactions.contains { + $0.userReacted && $0.reaction == reaction + } + } } private func setReaction(_ ci: ChatItem, add: Bool, reaction: MsgReaction) { @@ -911,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 { + 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 { + Button { if case let .image(text, _) = ci.content.msgContent, text == "", let image = getLoadedImage(ci.file) { @@ -936,57 +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) + } label: { + Label("Copy", systemImage: "doc.on.doc") } } - private func downloadFileAction(_ file: CIFile) -> UIAction { - UIAction( - title: NSLocalizedString("Download", comment: "chat item action"), - image: UIImage(systemName: "arrow.down.doc") - ) { _ in + func saveButton(image: UIImage) -> Button { + Button { + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + } label: { + Label( + NSLocalizedString("Save", comment: "chat item action"), + systemImage: "square.and.arrow.down" + ) + } + } + + func saveButton(file: CryptoFile) -> Button { + 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 { + 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 editAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Edit", comment: "chat item action"), - image: UIImage(systemName: "square.and.pencil") - ) { _ in + private func editButton(_ ci: ChatItem) -> Button { + Button { withAnimation { composeState = ComposeState(editingItem: ci) } + } label: { + Label( + NSLocalizedString("Edit", comment: "chat item action"), + systemImage: "square.and.pencil" + ) } } - private func viewInfoUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Info", comment: "chat item action"), - image: UIImage(systemName: "info.circle") - ) { _ in + private func selectButton(_ ci: ChatItem) -> Button { + 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 { + Button { Task { do { let cInfo = chat.chatInfo @@ -995,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 { + Button { AlertManager.shared.showAlert(Alert( title: Text(cancelAction.alert.title), message: Text(cancelAction.alert.message), @@ -1023,26 +1976,29 @@ 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 { + 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 + + private func deleteButton(_ ci: ChatItem, label: LocalizedStringKey = "Delete") -> Button { + Button(role: .destructive) { if !revealed, let currIndex = m.getChatItemIndex(ci), let ciCategory = ci.mergeCategory { @@ -1050,7 +2006,7 @@ struct ChatView: View { 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 @@ -1062,6 +2018,8 @@ struct ChatView: View { showDeleteMessage = true deletingItem = ci } + } label: { + Label(label, systemImage: "trash") } } @@ -1075,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 { + 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 { + Button { + archivingReports = [cItem.id] + showArchivingReports = true + } label: { + Label("Archive report", systemImage: "archivebox") + } + } + + private func revealButton(_ ci: ChatItem) -> Button { + 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 { + 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 { + 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 { + 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 { @@ -1139,105 +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? + @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 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))") } } - - private func scrollUp(_ proxy: ScrollViewProxy) { - if let ci = chatModel.topItemInView(itemsInView: itemsInView) { - withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } +} + +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 { + Image(systemName: "person.crop.circle") } } } +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 { - Button { - toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) - } label: { - if chat.chatInfo.ntfsEnabled { - Label("Mute", systemImage: "speaker.slash") - } else { - Label("Unmute", systemImage: "speaker.wave.2") + 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: Bool) { +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) } @@ -1252,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 @@ -1261,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))") @@ -1272,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 6cf9df782b..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,11 +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 forwardingItem(chatItem: ChatItem, fromChatInfo: ChatInfo) + case forwardingItems(chatItems: [ChatItem], fromChatInfo: ChatInfo) + case reportedItem(chatItem: ChatItem, reason: ReportReason) } enum VoiceMessageRecordingState { @@ -38,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, @@ -71,31 +84,51 @@ struct ComposeState { } else { self.voiceMessageRecordingState = .noRecording } + self.mentions = editingItem.mentions ?? [:] } - init(forwardingItem: ChatItem, fromChatInfo: ChatInfo) { + init(forwardingItems: [ChatItem], fromChatInfo: ChatInfo) { self.message = "" + self.parsedMessage = [] self.preview = .noPreview - self.contextItem = .forwardingItem(chatItem: forwardingItem, fromChatInfo: fromChatInfo) + 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 @@ -112,17 +145,35 @@ struct ComposeState { var forwarding: Bool { switch contextItem { - case .forwardingItem: return true + 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 || forwarding || liveMessage != nil + default: return !message.isEmpty || forwarding || liveMessage != nil || submittingValidReport } } @@ -167,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 || forwarding || liveMessage != nil || inProgress { return true } + if editing || forwarding || liveMessage != nil || inProgress || reporting { return true } switch preview { case .noPreview: return false case .linkPreview: return false @@ -186,6 +244,15 @@ struct ComposeState { } } + var placeholder: String? { + switch contextItem { + case let .reportedItem(_, reason): + return reason.text + default: + return nil + } + } + var empty: Bool { message == "" && noPreview } @@ -254,9 +321,13 @@ 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 @@ -279,22 +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) { @@ -309,10 +392,11 @@ 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(for: g.membership) { b.disabled(true).onTapGesture { @@ -327,6 +411,7 @@ struct ComposeView: View { ZStack(alignment: .leading) { SendMessageView( composeState: $composeState, + selectedRange: $selectedRange, sendMessage: { ttl in sendMessage(ttl: ttl) resetLinkPreview() @@ -351,45 +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." - ) - } } } } } + .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 msg.count > 0 { - showLinkPreview(msg) + showLinkPreview(parsedMsg) } else { resetLinkPreview() hasSimplexLink = false } } else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) { - (_, hasSimplexLink) = parseMessage(msg) + (_, 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() @@ -439,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)) @@ -531,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)) @@ -622,6 +708,7 @@ struct ComposeView: View { cancelPreview: cancelLinkPreview, cancelEnabled: !composeState.inProgress ) + Divider() case let .mediaPreviews(mediaPreviews: media): ComposeImageView( images: media.map { (img, _) in img }, @@ -630,6 +717,7 @@ struct ComposeView: View { chosenMedia = [] }, cancelEnabled: !composeState.editing && !composeState.inProgress) + Divider() case let .voicePreview(recordingFileName, _): ComposeVoiceView( recordingFileName: recordingFileName, @@ -642,6 +730,7 @@ struct ComposeView: View { cancelEnabled: !composeState.editing && !composeState.inProgress, stopPlayback: $stopPlayback ) + Divider() case let .filePreview(fileName, _): ComposeFileView( fileName: fileName, @@ -649,20 +738,41 @@ 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(.secondary) + Image(systemName: icon).foregroundColor(theme.colors.secondary) Text(reason).italic() } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .padding(.top, 8) + .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 { @@ -671,25 +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() } ) - case let .forwardingItem(chatItem: forwardedItem, _): + Divider() + case let .forwardingItems(chatItems, _): ContextItemView( chat: chat, - contextItem: forwardedItem, + contextItems: chatItems, contextIcon: "arrowshape.turn.up.forward", - cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }, - showSender: false + 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() } } @@ -705,21 +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 .forwardingItem(ci, fromChatInfo) = composeState.contextItem { - sent = await forwardItem(ci, fromChatInfo) + } 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 { - sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: nil) + _ = 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 { @@ -728,36 +853,39 @@ 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.. 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) { - sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl) + sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl, mentions: mentions) } } } @@ -772,6 +900,20 @@ struct ComposeView: View { } 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 } } @@ -798,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 { @@ -830,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) @@ -865,51 +993,93 @@ struct ComposeView: View { return nil } } + + 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(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { - if let chatItem = chat.chatInfo.chatType == .local - ? await apiCreateChatItem(noteFolderId: chat.chatInfo.apiId, file: file, msg: mc) - : await apiSendMessage( + 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, - file: file, - quotedItemId: quoted, - msg: mc, live: live, - ttl: ttl + ttl: ttl, + composedMessages: msgs ) { await MainActor.run { chatModel.removeLiveDummy(animated: false) - chatModel.addChatItem(chat.chatInfo, chatItem) + for chatItem in chatItems { + chatModel.addChatItem(chat.chatInfo, chatItem) + } } - return chatItem + return chatItems } - if let file = file { - removeFile(file.filePath) + for msg in msgs { + if let file = msg.fileSource { + removeFile(file.filePath) + } } - return nil + return [] } - func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo) async -> ChatItem? { - if let chatItem = await apiForwardChatItem( + 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, - itemId: forwardedItem.id + itemIds: forwardedItems.map { $0.id }, + ttl: ttl ) { await MainActor.run { - 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 [] } - return nil } func checkLinkPreview() -> MsgContent { switch (composeState.preview) { case let .linkPreview(linkPreview: linkPreview): - if let url = parseMessage(msgText).url, + if let parsedMsg = parseSimpleXMarkdown(msgText), + let url = getSimplexLink(parsedMsg).url, let linkPreview = linkPreview, url == linkPreview.uri { return .link(text: msgText, preview: linkPreview) @@ -920,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 { @@ -1036,9 +1198,9 @@ struct ComposeView: View { } } - private func showLinkPreview(_ s: String) { + private func showLinkPreview(_ parsedMsg: [FormattedText]?) { prevLinkUrl = linkUrl - (linkUrl, hasSimplexLink) = parseMessage(s) + (linkUrl, hasSimplexLink) = getSimplexLink(parsedMsg) if let url = linkUrl { if url != composeState.linkPreview?.uri && url != pendingLinkUrl { pendingLinkUrl = url @@ -1055,8 +1217,8 @@ struct ComposeView: View { } } - private func parseMessage(_ msg: String) -> (url: URL?, hasSimplexLink: Bool) { - guard let parsedMsg = parseSimpleXMarkdown(msg) else { return (nil, false) } + 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) }) { @@ -1064,7 +1226,7 @@ struct ComposeView: View { } else { nil } - let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) + let simplexLink = parsedMsgHasSimplexLink(parsedMsg) return (url, simplexLink) } @@ -1087,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 } } } @@ -1108,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 2777d8321c..845442c75f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -10,11 +10,12 @@ 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 { @@ -23,14 +24,23 @@ struct ContextItemView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) - .foregroundColor(.secondary) - if showSender, 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 { @@ -40,34 +50,45 @@ 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 { - contextMsgPreview() + 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() -> Text { - return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false) + 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 image("doc.fill") + case .file: return isFileLoaded ? image("doc.fill") : Text("") case .image: return image("photo") - case .voice: return image("play.fill") + case .voice: return isFileLoaded ? image("play.fill") : Text("") default: return Text("") } } func image(_ s: String) -> Text { - Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + textSpace } } } @@ -75,6 +96,6 @@ struct ContextItemView: View { 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 f2c7221835..d809fd7b76 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -16,18 +16,16 @@ struct NativeTextEditor: UIViewRepresentable { @Binding var disableEditing: Bool @Binding var height: CGFloat @Binding var focused: Bool + @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(text) field.autocapitalizationType = .sentences @@ -35,10 +33,9 @@ struct NativeTextEditor: UIViewRepresentable { if !disableEditing { text = newText field.textAlignment = alignment(text) - updateFont(field) + field.updateFont() // Speed up the process of updating layout, reduce jumping content on screen - updateHeight(field) - self.height = field.frame.size.height + field.updateHeight() } else { field.text = text } @@ -46,42 +43,32 @@ 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) { + func updateUIView(_ field: CustomUITextField, context: Context) { if field.markedTextRange == nil && field.text != text { field.text = text field.textAlignment = alignment(text) - updateFont(field) - updateHeight(field) + field.updateFont() + field.updateHeight(updateBindingNow: false) } - } - - 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) + if field.placeholder != placeholder { + field.placeholder = placeholder } - } - - private func updateFont(_ field: UITextView) { - let newFont = isShortEmoji(field.text) - ? (field.text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont) - : UIFont.preferredFont(forTextStyle: .body) - if field.font != newFont { - field.font = newFont + if field.selectedRange != selectedRange { + field.selectedRange = selectedRange } } } @@ -90,17 +77,26 @@ private func alignment(_ text: String) -> NSTextAlignment { isRightToLeft(text) ? .right : .left } -private class CustomUITextField: UITextView, UITextViewDelegate { +class CustomUITextField: UITextView, UITextViewDelegate { + var parent: NativeTextEditor? var height: Binding var newHeight: CGFloat = 0 var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in } var onFocusChanged: (Bool) -> Void = { focused in } - - init(height: Binding) { + + private let placeholderLabel: UILabel = UILabel() + + init(parent: NativeTextEditor?, height: Binding) { + 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") } @@ -113,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 @@ -171,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 @@ -202,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 + } } } @@ -216,6 +272,9 @@ struct NativeTextEditor_Previews: PreviewProvider{ disableEditing: Binding.constant(false), height: Binding.constant(100), focused: Binding.constant(false), + 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 8b528a201c..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 @@ -30,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 @@ -39,55 +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 { - NativeTextEditor( - text: $composeState.message, - disableEditing: $composeState.inProgress, - height: $teHeight, - focused: $keyboardVisible, - 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 { @@ -105,6 +111,8 @@ 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 @@ -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) } @@ -187,7 +195,7 @@ struct SendMessageView: View { composeState.endLiveDisabled || disableSendButton ) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .contextMenu{ sendButtonContextMenuItems() } @@ -224,10 +232,10 @@ struct SendMessageView: View { @ViewBuilder private func sendButtonContextMenuItems() -> some View { if composeState.liveMessage == nil, - !composeState.editing, - !composeState.forwarding { + !composeState.editing { if case .noContextItem = composeState.contextItem, !composeState.voicePreview, + !composeState.manyMediaPreviews, let send = sendLiveMessage, let update = updateLiveMessage { Button { @@ -248,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 @@ -255,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 { @@ -268,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 @@ -310,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) } @@ -324,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) @@ -341,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) @@ -384,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) } @@ -395,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) } @@ -414,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 { @@ -424,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 { @@ -434,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 86acbf6d54..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! @@ -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 { @@ -117,31 +122,15 @@ struct ContactPreferencesView: View { private func featureFooter(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View { Text(feature.enabledDescription(enabled)) } - - 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))") - } - } - } } 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: UIViewControllerRepresentable where ScrollItem : Identifiable, ScrollItem: Hashable { + + let scrollView: EndlessScrollView + 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 + fileprivate var items: [ScrollItem] = [] + fileprivate var content: ((Int, ScrollItem) -> Content)! + + fileprivate init(scrollView: EndlessScrollView, 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() : cellsToReuse.removeLast() as! HostingCell + 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 { + 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: 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.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.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.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.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 ed2afb91b3..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 { @@ -197,7 +214,7 @@ struct AddGroupMembersViewCommon: View { 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) @@ -207,7 +224,7 @@ struct AddGroupMembersViewCommon: View { } } -func searchFieldView(text: Binding, focussed: FocusState.Binding) -> some View { +func searchFieldView(text: Binding, focussed: FocusState.Binding, _ onBackgroundColor: Color, _ secondaryColor: Color) -> some View { HStack { Image(systemName: "magnifyingglass") .resizable() @@ -216,8 +233,9 @@ func searchFieldView(text: Binding, focussed: FocusState.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() @@ -228,7 +246,7 @@ func searchFieldView(text: Binding, focussed: FocusState.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 c22f3f0fed..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 @@ -39,7 +45,7 @@ struct GroupChatInfoView: View { 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 { @@ -64,78 +70,112 @@ struct GroupChatInfoView: View { .filter { m in let status = m.wrapped.memberStatus; return status != .memLeft && status != .memRemoved } .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 @@ -150,7 +190,7 @@ struct GroupChatInfoView: View { 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 { @@ -166,7 +206,6 @@ struct GroupChatInfoView: View { logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))") } } - .keyboardPadding() } private func groupInfoHeader() -> some View { @@ -175,7 +214,7 @@ struct GroupChatInfoView: View { 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) @@ -190,49 +229,167 @@ 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, size: 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() memberInfo(member) } - + + let v = ZStack { + if user { + v1 + } else { + NavigationLink { + memberInfoView() + } label: { + EmptyView() + } + .opacity(0) + v1 + } + } + if user { v } else if groupInfo.membership.memberRole >= .admin { @@ -257,15 +414,30 @@ struct GroupChatInfoView: View { } } + 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(.secondary) + .foregroundColor(theme.colors.secondary) } else { let role = member.memberRole if [.owner, .admin, .observer].contains(role) { Text(member.memberRole.text) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } @@ -276,13 +448,13 @@ 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) } } } @@ -294,13 +466,13 @@ struct GroupChatInfoView: View { Button { alert = .unblockForAllAlert(mem: member) } label: { - Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor) + 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(.secondary) + Label("Block for all", systemImage: "hand.raised").foregroundColor(theme.colors.secondary) } } } @@ -316,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") @@ -343,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( @@ -350,6 +530,7 @@ struct GroupChatInfoView: View { groupProfile: groupInfo.groupProfile ) .navigationBarTitle("Group profile") + .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.large) } label: { Label("Edit group profile", systemImage: "pencil") @@ -364,6 +545,7 @@ struct GroupChatInfoView: View { welcomeText: groupInfo.groupProfile.description ?? "" ) .navigationTitle("Welcome message") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { groupInfo.groupProfile.description == nil @@ -372,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) } } @@ -391,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 { @@ -422,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?"), @@ -441,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) @@ -487,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) } @@ -509,33 +703,80 @@ struct GroupChatInfoView: View { } } -func groupPreferencesButton(_ groupInfo: Binding, _ 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!"), @@ -554,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 c782e2a717..a11c073a42 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -10,19 +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 { @@ -34,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() } @@ -70,10 +71,10 @@ struct GroupLinkView: View { } } .frame(height: 36) - SimpleXLinkQRCode(uri: groupLink) - .id("simplex-qrcode-view-for-\(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") } @@ -94,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 { @@ -113,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 @@ -133,6 +138,7 @@ struct GroupLinkView: View { shouldCreate = false } } + .modifier(ThemedBackground(grouped: true)) } private func createGroupLink() { @@ -158,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 a24608b7e7..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? @@ -35,7 +41,9 @@ struct GroupMemberInfoView: View { 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 { @@ -49,6 +57,8 @@ struct GroupMemberInfoView: View { 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)" } } @@ -62,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 } @@ -73,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(for: groupInfo.membership) { - 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 { @@ -109,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(for: groupInfo.membership) { + if member.memberContactId != nil { + if knownContactChat == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { connectViaAddressButton(contactLink) } } else { @@ -118,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) { @@ -136,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") { @@ -160,11 +170,11 @@ 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) } } @@ -175,41 +185,74 @@ struct GroupMemberInfoView: View { } 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) { @@ -223,7 +266,9 @@ struct GroupMemberInfoView: View { 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) } @@ -232,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 { @@ -248,72 +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, size: 192, color: Color(uiColor: .tertiarySystemFill)) + 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", @@ -408,7 +546,7 @@ struct GroupMemberInfoView: View { Section { if mem.blockedByAdmin { Label("Blocked by admin", systemImage: "hand.raised") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } else if mem.memberSettings.showMessages { blockMemberButton(mem) } else { @@ -461,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) } @@ -486,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) } @@ -566,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?"), @@ -634,12 +804,14 @@ func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) { Task { do { - let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked) + let updatedMembers = try await apiBlockMembersForAll(gInfo.groupId, [member.groupMemberId], blocked) await MainActor.run { - _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + updatedMembers.forEach { updatedMember in + _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + } } } catch let error { - logger.error("apiBlockMemberForAll error: \(responseError(error))") + logger.error("apiBlockMembersForAll error: \(responseError(error))") } } } @@ -648,6 +820,7 @@ 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 = 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 b4e1992848..ed39c401ce 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -18,10 +18,12 @@ private let featureRoles: [(role: GroupMemberRole?, text: LocalizedStringKey)] = 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 { @@ -34,11 +36,11 @@ struct GroupPreferencesView: View { featureSection(.reactions, $preferences.reactions.enable) featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) featureSection(.files, $preferences.files.enable, $preferences.files.role) - // TODO enable simplexLinks preference in 5.8 - // featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.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() } @@ -68,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, _ enableForRole: Binding? = 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 } @@ -85,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, @@ -102,8 +108,6 @@ struct GroupPreferencesView: View { } } .frame(height: 36) - // remove in v5.8 - .disabled(true) } } else { settingsRow(icon, color: color) { @@ -114,18 +118,19 @@ struct GroupPreferencesView: View { } if enableFeature.wrappedValue == .on, let enableForRole { HStack { - Text("Enabled for").foregroundColor(.secondary) + Text("Enabled for").foregroundColor(theme.colors.secondary) Spacer() Text( featureRoles.first(where: { fr in fr.role == enableForRole.wrappedValue })?.text ?? "all members" ) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } } footer: { - Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit)) + Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner)) + .foregroundColor(theme.colors.secondary) } .onChange(of: enableFeature.wrappedValue) { enabled in if case .off = enabled { @@ -133,32 +138,16 @@ struct GroupPreferencesView: View { } } } - - 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))") - } - } - } } 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 00d4f8c37b..97bff70efb 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -11,18 +11,20 @@ import SimpleXChat struct GroupWelcomeView: View { @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme @Binding var groupInfo: GroupInfo @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 = [] let maxByteCount = 1200 var body: some View { VStack { - if groupInfo.canEdit { + if groupInfo.isOwner && groupInfo.businessChat == nil { editorView() .modifier(BackButton(disabled: Binding.constant(false)) { if welcomeTextUnchanged() { @@ -57,7 +59,8 @@ struct GroupWelcomeView: View { } private func textPreview() -> some View { - messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false) + 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) } @@ -70,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) 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: UIView, ReusableView { + private let hostingController = UIHostingController(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? + + 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? + 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?) { + 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 2435c9a4f5..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 newChatMenuOption: NewChatMenuOption? = nil + 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,11 +38,12 @@ struct ChatHelp: View { HStack(spacing: 8) { Text("Tap button ") - NewChatMenuButton(newChatMenuOption: $newChatMenuOption) + NewChatMenuButton() Text("above, then choose:") } - Text("**Add contact**: to create a new invitation link, or connect via a link you received.") + 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) @@ -62,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 efe254323e..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? @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? = 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 { @@ -63,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) } @@ -85,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: 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 } } } @@ -138,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 { @@ -158,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) } @@ -172,39 +198,59 @@ 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: 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) + } } } } } - @ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View { + private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View { NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !noteFolder.ready ) - .frame(height: rowHeights[dynamicTypeSize]) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() } @@ -222,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 { @@ -232,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) } } @@ -251,33 +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: { - Label("Clear", systemImage: "gobackward") + SwipeLabel(NSLocalizedString("Clear", comment: "swipe action"), systemImage: "gobackward", inverted: oneHandUI) } .tint(Color.orange) } @@ -286,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) } @@ -295,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) } } @@ -331,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, nil) + .appSheet(isPresented: $showContactConnectionInfo) { + Group { + if case let .contactConnection(contactConnection) = chat.chatInfo { + ContactConnectionInfo(contactConnection: contactConnection) + .environment(\EnvironmentValues.refresh as! WritableKeyPath, 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) } @@ -371,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 { @@ -398,9 +556,15 @@ struct ChatListNavLink: View { } private func leaveGroupAlert(_ groupInfo: GroupInfo) -> 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(groupInfo.groupId) } }, @@ -408,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"), @@ -437,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, 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?"), @@ -511,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 } @@ -560,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))") } } @@ -583,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 6bf63bb2e3..f34f930c6f 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -9,19 +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 searchShowingSimplexLink = false @State private var searchChatFilteredBySimplexLink: String? = nil - @State private var newChatMenuOption: NewChatMenuOption? = nil - @State private var userPickerVisible = false - @State private var showConnectDesktop = false - @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false + @State private var scrollToSearchBar = false + @State private var userPickerShown: Bool = false + @State private var sheet: SomeSheet? = 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) @@ -29,46 +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 { + 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,65 +233,112 @@ struct ChatListView: View { secondaryButton: .cancel() )) } - .listStyle(.plain) - .navigationBarTitleDisplayMode(.inline) - .navigationBarHidden(searchMode) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - let user = chatModel.currentUser ?? User.sampleData - ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel)) - .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) - } - } - .onTapGesture { - if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { - withAnimation { - userPickerVisible.toggle() - } - } else { - showSettings = true - } - } + .safeAreaInset(edge: .top) { + if oneHandUI { Divider().background(tm) } + } + .safeAreaInset(edge: .bottom) { + if oneHandUI { + Divider().padding(.bottom, Self.hasHomeIndicator ? 0 : 8).background(tm) } - ToolbarItem(placement: .principal) { - HStack(spacing: 4) { - Text("Chats") - .font(.headline) - if chatModel.chats.count > 0 { - toggleFilterButton() - } - } - .frame(maxWidth: .infinity, alignment: .center) - } - ToolbarItem(placement: .navigationBarTrailing) { - switch chatModel.chatRunning { - case .some(true): NewChatMenuButton(newChatMenuOption: $newChatMenuOption) - case .some(false): chatStoppedIcon() - case .none: EmptyView() - } + } + .sheet(item: $sheet) { sheet in + if #available(iOS 16.0, *) { + sheet.content.presentationDetents([.fraction(sheet.fraction)]) + } else { + sheet.content } } } - - private func toggleFilterButton() -> some View { - Button { - showUnreadAndFavorites = !showUnreadAndFavorites - } label: { - Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : "")) - .foregroundColor(.accentColor) + + 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 } + } } } - - @ViewBuilder private var chatList: some View { + + @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() - ZStack { - VStack { + return ZStack { + ScrollViewReader { scrollProxy in List { if !chatModel.chats.isEmpty { ChatListSearchBar( @@ -151,148 +346,242 @@ struct ChatListView: View { searchFocussed: $searchFocussed, searchText: $searchText, searchShowingSimplexLink: $searchShowingSimplexLink, - searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + 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") } - ForEach(cs, id: \.viewId) { chat in - ChatListNavLink(chat: chat) - .padding(.trailing, -16) + 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 { + 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) } - .offset(x: -8) } - } - .onChange(of: chatModel.chatId) { _ in - if chatModel.chatId == nil, let chatId = chatModel.chatToTop { - chatModel.chatToTop = nil - chatModel.popChat(chatId) + .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") } + } } } if cs.isEmpty && !chatModel.chats.isEmpty { - Text("No filtered chats").foregroundColor(.secondary) + 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(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View { + + private func unreadBadge(size: CGFloat = 18) -> some View { Circle() .frame(width: size, height: size) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } - - 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") { - newChatMenuOption = .newContact - } - - 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) - } + 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 == "" && !showUnreadAndFavorites - ? chatModel.chats + return s == "" + ? chatModel.chats.filter { chat in + !chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard && filtered(chat) + } : chatModel.chats.filter { chat in let cInfo = chat.chatInfo - switch cInfo { + return 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 .local: - return s == "" || 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 + !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 searchString() -> String { - searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase - } - + func filtered(_ chat: Chat) -> Bool { - (chat.chatInfo.chatSettings?.favorite ?? false) || - chat.chatStats.unreadChat || - (chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0) + 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? + @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, 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? @State private var ignoreSearchTextChange = false - @State private var showScanCodeSheet = 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) { Image(systemName: "magnifyingglass") TextField("Search or paste SimpleX link", text: $searchText) - .foregroundColor(searchShowingSimplexLink ? .secondary : .primary) + .foregroundColor(searchShowingSimplexLink ? theme.colors.secondary : theme.colors.onBackground) .disabled(searchShowingSimplexLink) .focused($searchFocussed) .frame(maxWidth: .infinity) @@ -301,47 +590,24 @@ struct ChatListSearchBar: View { .onTapGesture { searchText = "" } - } else if !searchFocussed { - HStack(spacing: 24) { - if m.pasteboardHasStrings { - Image(systemName: "doc") - .onTapGesture { - if let str = UIPasteboard.general.string { - searchText = str - } - } - } - - Image(systemName: "qrcode") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .onTapGesture { - showScanCodeSheet = true - } - } - .padding(.trailing, 2) } } .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .background(Color(.tertiarySystemFill)) .cornerRadius(10.0) if searchFocussed { Text("Cancel") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .onTapGesture { searchText = "" searchFocussed = false } + } else if m.chats.count > 0 { + toggleFilterButton() } } - Divider() - } - .sheet(isPresented: $showScanCodeSheet) { - NewChatView(selection: .connect, showQRCodeScanner: true) - .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) // fixes .refreshable in ChatListView affecting nested view } .onChange(of: searchFocussed) { sf in withAnimation { searchMode = sf } @@ -368,6 +634,9 @@ struct ChatListSearchBar: View { } } } + .onChange(of: chatTagsModel.activeFilter) { _ in + searchText = "" + } .alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" }) } @@ -376,6 +645,26 @@ struct ChatListSearchBar: View { } } + 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, @@ -389,6 +678,198 @@ struct ChatListSearchBar: View { } } +struct TagsView: View { + @EnvironmentObject var chatTagsModel: ChatTagsModel + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var parentSheet: SomeSheet? + @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 { + parentSheet = SomeSheet( + content: { + AnyView( + NavigationView { + TagListEditor() + } + ) + }, + id: "tag create" + ) + } label: { + 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)) + } + } + + 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") + } + } + } + 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) + } + } + } + } + } 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 setActiveFilter(filter: ActiveFilter) { + if filter != chatTagsModel.activeFilter { + chatTagsModel.activeFilter = filter + } else { + chatTagsModel.activeFilter = nil + } + } +} + func chatStoppedIcon() -> some View { Button { AlertManager.shared.showAlertMsg( @@ -400,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 fe8fd8b28e..b8c8233e6e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -11,52 +11,125 @@ 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 @State var deleting: Bool = false - @Environment(\.colorScheme) var colorScheme 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, size: 63) - chatPreviewImageOverlayIcon() - .padding([.bottom, .trailing], 1) - } - .padding(.leading, 4) - - VStack(spacing: 0) { - HStack(alignment: .top) { - chatPreviewTitle() - Spacer() - (cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs)) - .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) } - .frame(maxHeight: .infinity) } - .padding(.bottom, -8) .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 + } } } @@ -70,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() @@ -80,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))) @@ -94,9 +168,9 @@ struct ChatPreviewView: View { case let .group(groupInfo): let v = previewTitle(t) switch (groupInfo.membership.memberStatus) { - case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : .accentColor) - case .memAccepted: v.foregroundColor(.secondary) - default: if deleting { v.foregroundColor(.secondary) } else { 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) } @@ -107,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 || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary) - .cornerRadius(10) - } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { - 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, showSecrets: false) + 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("") @@ -169,19 +282,24 @@ struct ChatPreviewView: View { } } - func chatItemPreview(_ cItem: ChatItem) -> 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, showSecrets: false) + 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 { - 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") + 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") + } } } @@ -194,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 { @@ -216,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() @@ -225,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) @@ -240,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 42e90232d6..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,13 +173,11 @@ struct ContactConnectionInfo: View { } } -private func shareLinkButton(_ connReqInvitation: String) -> some View { +private func shareLinkButton(_ connLinkInvitation: CreatedConnLink, short: Bool) -> some View { Button { - showShareSheet(items: [simplexChatLink(connReqInvitation)]) + showShareSheet(items: [connLinkInvitation.simplexChatUri(short: short)]) } label: { - settingsRow("square.and.arrow.up") { - Text("Share 1-time link") - } + Label("Share 1-time link", systemImage: "square.and.arrow.up") } } @@ -178,11 +185,10 @@ private func oneTimeLinkLearnMoreButton() -> some View { NavigationLink { AddContactLearnMore(showTitle: false) .navigationTitle("One-time invitation link") + .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.large) } label: { - settingsRow("info.circle") { - Text("Learn more") - } + Label("Learn more", systemImage: "info.circle") } } 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 dacf51a5e8..9276bbfc78 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -11,19 +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, size: 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() @@ -32,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(_ 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.. 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(\.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 { - // This check prevents the call of listUsers after the app is suspended, and the database is closed. - if case .active = scenePhase { - 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, size: 44, color: Color(uiColor: .tertiarySystemFill)) - .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? + + @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? = 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 4031c3e00a..441a164f8a 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -35,6 +35,7 @@ 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 @@ -47,6 +48,8 @@ struct DatabaseEncryptionView: View { @State private var confirmNewKey = "" @State private var currentKeyShown = false + let stopChatRunBlockStartChat: (Binding, @escaping () async throws -> Bool) -> Void + var body: some View { ZStack { List { @@ -63,7 +66,7 @@ struct DatabaseEncryptionView: View { private func databaseEncryptionView() -> some View { Section { - settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) { + settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : theme.colors.secondary) { Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle) .onChange(of: useKeychainToggle) { _ in if useKeychainToggle { @@ -85,7 +88,7 @@ struct DatabaseEncryptionView: View { 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") { + settingsRow("lock.rotation", color: theme.colors.secondary) { Button(migration ? "Set passphrase" : "Update database passphrase") { alert = currentKey == "" ? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase) @@ -102,6 +105,7 @@ struct DatabaseEncryptionView: View { ) } header: { Text(migration ? "Database passphrase" : "") + .foregroundColor(theme.colors.secondary) } footer: { VStack(alignment: .leading, spacing: 16) { if m.chatDbEncrypted == false { @@ -125,52 +129,68 @@ struct DatabaseEncryptionView: View { } } } + .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) - 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(.error(title: "Keychain error", error: "Error saving passphrase to keychain")) - } - } else { - if migration { - removePassphraseFromKeyChain() - } - await resetFormAfterEncryption() + 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) - } - } 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))")) + await resetFormAfterEncryption() + await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain")) } + } 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() } } } @@ -277,6 +297,7 @@ struct DatabaseEncryptionView: View { struct PassphraseField: View { + @EnvironmentObject var theme: AppTheme @Binding var key: String var placeholder: LocalizedStringKey var valid: Bool @@ -287,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() @@ -367,6 +388,6 @@ func validKey(_ s: String) -> Bool { struct DatabaseEncryptionView_Previews: PreviewProvider { static var previews: some View { - DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false) + 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 f8d282a6d1..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(DatabaseErrorView.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: ", "))") - } - - static 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() { @@ -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 2e0cd7738f..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, migration: false) + 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, + _ 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, _ progressIndicator: Binding) { + 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, + _ alert: Binding, + _ 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, _ alert: Binding) 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() @@ -492,7 +642,9 @@ func deleteChatAsync() async throws { } 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 ae6af24f53..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) @@ -189,7 +189,8 @@ struct MigrateToAppGroupView: View { Task { do { try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) - try await apiExportArchive(config: config) + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + _ = try await apiExportArchive(config: config) await MainActor.run { setV3DBMigration(.exported) } } catch let error { await MainActor.run { @@ -221,7 +222,7 @@ struct MigrateToAppGroupView: View { } } -func exportChatArchive(_ storagePath: URL? = nil) 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" @@ -231,16 +232,19 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL { if !ChatModel.shared.chatDbChanged { try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) } - try await apiExportArchive(config: config) + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + let errs = try await apiExportArchive(config: config) if storagePath == nil { - deleteOldArchive() + deleteOldChatArchive() UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME) chatArchiveTimeDefault.set(archiveTime) } - return archivePath + 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: ViewModifier where C: View { - var isPresented: Binding - 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: ViewModifier where T: Identifiable, C: View { - var item: Binding - 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( @@ -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 0180b066ab..40d62e009b 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -10,25 +10,16 @@ import SwiftUI import SimpleXChat struct ChatInfoImage: View { - @Environment(\.colorScheme) var colorScheme + @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 .local: iconName = "folder.circle.fill" - case .contactRequest: iconName = "person.crop.circle.fill" - default: iconName = "circle.fill" - } - let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark - let iconColor = if case .local = chat.chatInfo { notesColor } else { color } + let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color } return ProfileImage( imageStr: chat.chatInfo.image, - iconName: iconName, + iconName: chatIconName(chat.chatInfo), size: size, color: iconColor ) 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 9504d919ef..0000000000 --- a/apps/ios/Shared/Views/Helpers/ContextMenu.swift +++ /dev/null @@ -1,112 +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(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding, menu: Binding, allowMenu: Binding) -> some View { - Group { - if allowMenu.wrappedValue { - if hasImageOrVideo { - InteractionView(content: - self.environmentObject(ChatModel.shared) - .overlay(DetermineWidthImageVideoItem()) - .onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 } - , maxWidth: maxWidth, itemWidth: itemWidth, menu: menu) - .frame(maxWidth: itemWidth.wrappedValue) - } else { - InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu) - .fixedSize(horizontal: true, vertical: false) - } - } else { - self - } - } - } -} - -private class HostingViewHolder: UIView { - var contentSize: CGSize = CGSizeMake(0, 0) - override var intrinsicContentSize: CGSize { get { contentSize } } -} - -struct InteractionView: UIViewRepresentable { - let content: Content - var maxWidth: CGFloat - var itemWidth: Binding - @Binding var menu: UIMenu - - func makeUIView(context: Context) -> UIView { - let view = HostingViewHolder() - view.backgroundColor = .clear - let hostView = UIHostingController(rootView: content) - view.contentSize = hostView.view.intrinsicContentSize - 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) - view.layer.cornerRadius = 18 - hostView.view.layer.cornerRadius = 18 - let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator) - view.addInteraction(menuInteraction) - return view - } - - func updateUIView(_ uiView: UIView, context: Context) { - let was = (uiView as! HostingViewHolder).contentSize - (uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity)) - if was != (uiView as! HostingViewHolder).contentSize { - uiView.invalidateIntrinsicContentSize() - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, UIContextMenuInteractionDelegate { - let parent: InteractionView - - init(_ parent: InteractionView) { - 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/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: View { - @State var tag: V - @Binding var selection: V? +struct NavLinkPlain: 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 : 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 6b8439504a..3eedd56441 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -9,45 +9,49 @@ import SwiftUI import SimpleXChat -let defaultProfileImageCorner = 22.5 - 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) { - clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius) + 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)) } } } -private let squareToCircleRatio = 0.935 +extension Color { + func asAnotherColorFromSecondary(_ theme: AppTheme) -> Color { + return self + } -private let radiusFactor = (1 - squareToCircleRatio) / 50 - -@ViewBuilder func clipProfileImage(_ img: Image, size: CGFloat, radius: Double) -> some View { - let v = img.resizable() - if radius >= 50 { - v.frame(width: size, height: size).clipShape(Circle()) - } else if radius <= 0 { - let sz = size * squareToCircleRatio - v.frame(width: sz, height: sz).padding((size - sz) / 2) - } else { - let sz = size * (squareToCircleRatio + radius * radiusFactor) - v.frame(width: sz, height: sz) - .clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous)) - .padding((size - sz) / 2) + 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 + } } } 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: 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: UIViewControllerRepresentable { + @Binding var isPresented: Bool + let content: Content + + func makeUIViewController(context: Context) -> Controller { + Controller(content: content, representer: self) + } + + func updateUIViewController(_ sheetController: Controller, context: Context) { + sheetController.animate(isPresented: isPresented) + } + + class Controller: UIViewController { + let hostingController: UIHostingController + private let animator = UIViewPropertyAnimator( + duration: sheetAnimationDuration, + timingParameters: easeOutCubic + ) + private let representer: SheetRepresentable + private var retainedFraction: CGFloat = 0 + private var sheetHeight: Double { hostingController.view.frame.height } + private var task: Task? + + init(content: C, representer: SheetRepresentable) { + 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: 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 + + init(content: Content) { + self.hostingController = UIHostingController(rootView: content) + } + + func scrollViewWillEndDragging( + _ scrollView: UIScrollView, + withVelocity velocity: CGPoint, + targetContentOffset: UnsafeMutablePointer + ) { + 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 { + 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, 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 { + 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: 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`(_ 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 9691a9efd3..16ab26eff7 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -64,8 +64,9 @@ struct LocalAuthView: View { deleteAppDatabaseAndFiles() // Clear sensitive data on screen just in case app fails to hide its views while new database is created m.chatId = nil - m.reversedChatItems = [] - m.chats = [] + ItemsModel.shared.reversedChatItems = [] + ItemsModel.shared.chatState.clear() + m.updateChats([]) m.users = [] _ = kcAppPassword.set(password) _ = kcSelfDestructPassword.remove() 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 9e0d7f38b5..ca30fa5ce8 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift @@ -29,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 { diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index 645de4c3f8..0af8fa7ad8 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -24,6 +24,7 @@ private enum MigrationFromState: Equatable { } 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") @@ -32,11 +33,13 @@ private enum MigrateFromDeviceViewAlert: Identifiable { 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)" @@ -45,6 +48,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable { 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)" } @@ -53,8 +57,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable { struct MigrateFromDevice: View { @EnvironmentObject var m: ChatModel - @Environment(\.dismiss) var dismiss: DismissAction - @Binding var showSettings: Bool + @EnvironmentObject var theme: AppTheme @Binding var showProgressOnSettings: Bool @State private var migrationState: MigrationFromState = .chatStopInProgress @State private var useKeychain = storeDBPassphraseGroupDefault.get() @@ -103,9 +106,6 @@ struct MigrateFromDevice: View { finishedView(chatDeletion) } } - .modifier(BackButton(label: "Back", disabled: $backDisabled) { - dismiss() - }) .onChange(of: migrationState) { state in backDisabled = switch migrationState { case .chatStopInProgress, .archiving, .linkShown, .finished: true @@ -135,6 +135,15 @@ struct MigrateFromDevice: View { } .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), @@ -165,6 +174,14 @@ struct MigrateFromDevice: View { 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)) } @@ -177,6 +194,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Stopping chat") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -188,21 +206,24 @@ struct MigrateFromDevice: View { Section { Text(reason) Button(action: stopChat) { - settingsRow("stop.fill") { + 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) + DatabaseEncryptionView(useKeychain: $useKeychain, migration: true, stopChatRunBlockStartChat: { _, _ in + }) .onChange(of: initialRandomDBPassphrase) { initial in if !initial { migrationState = .uploadConfirmation @@ -214,14 +235,16 @@ struct MigrateFromDevice: View { List { Section { Button(action: { migrationState = .archiving }) { - settingsRow("tray.and.arrow.up") { - Text("Archive and upload").foregroundColor(.accentColor) + 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) } } @@ -232,6 +255,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Archiving database") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -246,10 +270,11 @@ struct MigrateFromDevice: View { 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") + MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded", theme.colors.primary) } .onAppear { startUploading(totalBytes, archivePath) @@ -262,14 +287,16 @@ struct MigrateFromDevice: View { Button(action: { migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) }) { - settingsRow("tray.and.arrow.up") { - Text("Repeat upload").foregroundColor(.accentColor) + 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) } } @@ -283,6 +310,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Creating archive link") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -293,13 +321,13 @@ struct MigrateFromDevice: View { List { Section { Button(action: { cancelMigration(fileId, ctrl) }) { - settingsRow("multiply") { + settingsRow("multiply", color: theme.colors.secondary) { Text("Cancel migration").foregroundColor(.red) } } - Button(action: { finishMigration(fileId, ctrl) }) { - settingsRow("checkmark") { - Text("Finalize migration").foregroundColor(.accentColor) + Button(action: { alert = .finishMigration(fileId, ctrl) }) { + settingsRow("checkmark", color: theme.colors.secondary) { + Text("Finalize migration").foregroundColor(theme.colors.primary) } } } footer: { @@ -307,9 +335,10 @@ struct MigrateFromDevice: View { 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("Show QR code") { + Section(header: Text("Show QR code").foregroundColor(theme.colors.secondary)) { SimpleXLinkQRCode(uri: link) .padding() .background( @@ -322,7 +351,7 @@ struct MigrateFromDevice: View { .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } - Section("Or securely share this file link") { + 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)) @@ -334,22 +363,24 @@ struct MigrateFromDevice: View { List { Section { Button(action: { alert = .startChat() }) { - settingsRow("play.fill") { + settingsRow("play.fill", color: theme.colors.secondary) { Text("Start chat").foregroundColor(.red) } } Button(action: { alert = .deleteChat() }) { - settingsRow("trash.fill") { - Text("Delete database from this device").foregroundColor(.accentColor) + 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) } } @@ -379,7 +410,7 @@ struct MigrateFromDevice: View { .truncationMode(.middle) } - static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View { + static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey, _ primaryColor: Color) -> some View { ZStack { VStack { Text(description) @@ -389,7 +420,7 @@ struct MigrateFromDevice: View { Text(title) .font(.system(size: 54)) .bold() - .foregroundColor(.accentColor) + .foregroundColor(primaryColor) Text(description) .font(.title3) @@ -398,7 +429,7 @@ struct MigrateFromDevice: View { Circle() .trim(from: 0, to: CGFloat(value)) .stroke( - Color.accentColor, + primaryColor, style: StrokeStyle(lineWidth: 27) ) .rotationEffect(.degrees(180)) @@ -435,15 +466,12 @@ struct MigrateFromDevice: View { Task { do { try? FileManager.default.createDirectory(at: getMigrationTempFilesDirectory(), withIntermediateDirectories: true) - let archivePath = try await exportChatArchive(getMigrationTempFilesDirectory()) - 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) - } + let (archivePath, errs) = try await exportChatArchive(getMigrationTempFilesDirectory()) + if errs.isEmpty { + await uploadArchive(path: archivePath) } else { await MainActor.run { - alert = .error(title: "Exported file doesn't exist") + alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: errs) migrationState = .uploadConfirmation } } @@ -455,6 +483,20 @@ struct MigrateFromDevice: View { } } } + + 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) @@ -478,19 +520,25 @@ struct MigrateFromDevice: View { chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in await MainActor.run { switch msg { - case let .sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize): + 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 .sndFileRedirectStartXFTP: + case .result(.sndFileRedirectStartXFTP): DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { migrationState = .linkCreation } - case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs): + 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 ) @@ -498,7 +546,7 @@ struct MigrateFromDevice: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { migrationState = .linkShown(fileId: fileTransferMeta.fileId, link: data.addToLink(link: rcvURIs[0]), archivePath: archivePath, ctrl: ctrl) } - case .sndFileError: + case .result(.sndFileError): alert = .error(title: "Upload failed", error: "Check your internet connection and try again") migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath) default: @@ -555,7 +603,7 @@ struct MigrateFromDevice: View { } catch let error { fatalError("Error starting chat \(responseError(error))") } - showSettings = false + dismissAllSheets(animated: true) } } catch let error { alert = .error(title: "Error deleting database", error: responseError(error)) @@ -578,9 +626,7 @@ struct MigrateFromDevice: View { } // Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered if dismiss || m.chatDbStatus != .ok { - await MainActor.run { - showSettings = false - } + dismissAllSheets(animated: true) } } @@ -590,6 +636,7 @@ struct MigrateFromDevice: View { } private struct PassphraseConfirmationView: View { + @EnvironmentObject var theme: AppTheme @Binding var migrationState: MigrationFromState @State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var currentKey: String = "" @@ -612,15 +659,17 @@ private struct PassphraseConfirmationView: View { verifyingPassphrase = false } }) { - settingsRow(useKeychain ? "key" : "lock", color: .secondary) { + 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 { @@ -642,10 +691,10 @@ private struct PassphraseConfirmationView: View { migrationState = .uploadConfirmation } } catch let error { - if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse { + if case .errorDatabase(.errorOpen(.errorNotADatabase)) = error as? ChatError { showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert) } else { - alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(String(describing: error))) + alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(responseError(error))) } } } @@ -684,11 +733,11 @@ func chatStoppedView() -> some View { private class MigrationChatReceiver { let ctrl: chat_ctrl let databaseUrl: URL - let processReceivedMsg: (ChatResponse) async -> Void + let processReceivedMsg: (APIResult) async -> Void private var receiveLoop: Task? private var receiveMessages = true - init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatResponse) async -> Void) { + init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (APIResult) async -> Void) { self.ctrl = ctrl self.databaseUrl = databaseUrl self.processReceivedMsg = processReceivedMsg @@ -703,9 +752,9 @@ private class MigrationChatReceiver { func receiveMsgLoop() async { // TODO use function that has timeout - if let msg = await chatRecvMsg(ctrl) { + if let msg: APIResult = await chatRecvMsg(ctrl) { Task { - await TerminalItems.shared.add(.resp(.now, msg)) + await TerminalItems.shared.addResult(msg) } logger.debug("processReceivedMsg: \(msg.responseType)") await processReceivedMsg(msg) @@ -729,6 +778,6 @@ private class MigrationChatReceiver { struct MigrateFromDevice_Previews: PreviewProvider { static var previews: some View { - MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false)) + MigrateFromDevice(showProgressOnSettings: Binding.constant(false)) } } diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index e290537b46..93fe19cf33 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -91,16 +91,21 @@ private enum MigrateToDeviceViewAlert: Identifiable { struct MigrateToDevice: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction - @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @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 { @@ -174,13 +179,27 @@ struct MigrateToDevice: View { 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("Scan QR code") { + Section(header: Text("Scan QR code").foregroundColor(theme.colors.secondary)) { ScannerInView(showQRCodeScanner: $showQRCodeScanner) { resp in switch resp { case let .success(r): @@ -196,11 +215,15 @@ struct MigrateToDevice: View { } } } - if developerTools { - Section("Or paste archive link") { - pasteLinkView() - } + 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() } } } @@ -217,15 +240,44 @@ struct MigrateToDevice: View { } label: { Text("Tap to paste link") } - .disabled(!ChatModel.shared.pasteboardHasStrings) + .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() @@ -240,10 +292,11 @@ struct MigrateToDevice: View { 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") + MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded", theme.colors.primary) } } @@ -254,14 +307,16 @@ struct MigrateToDevice: View { try? FileManager.default.removeItem(atPath: archivePath) migrationState = .linkDownloading(link: link) }) { - settingsRow("tray.and.arrow.down") { - Text("Repeat download").foregroundColor(.accentColor) + 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) } } @@ -277,6 +332,7 @@ struct MigrateToDevice: View { List { Section {} header: { Text("Importing archive") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -292,14 +348,16 @@ struct MigrateToDevice: View { Button(action: { migrationState = .archiveImport(archivePath: archivePath) }) { - settingsRow("square.and.arrow.down") { - Text("Repeat import").foregroundColor(.accentColor) + 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) } } @@ -323,7 +381,7 @@ struct MigrateToDevice: View { case let .migrationError(mtrError): ("Incompatible database version", nil, - "\(NSLocalizedString("Error: ", comment: "")) \(DatabaseErrorView.mtrErrorDescription(mtrError))", + "\(NSLocalizedString("Error: ", comment: "")) \(mtrErrorDescription(mtrError))", nil) } default: ("Error", nil, "Unknown error", nil) @@ -333,8 +391,8 @@ struct MigrateToDevice: View { Button(action: { migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain) }) { - settingsRow("square.and.arrow.down") { - Text(button).foregroundColor(.accentColor) + settingsRow("square.and.arrow.down", color: theme.colors.secondary) { + Text(button).foregroundColor(theme.colors.primary) } } } else { @@ -342,8 +400,10 @@ struct MigrateToDevice: View { } } header: { Text(header) + .foregroundColor(theme.colors.secondary) } footer: { Text(footer) + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -354,6 +414,7 @@ struct MigrateToDevice: View { List { Section {} header: { Text("Migrating") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -364,6 +425,7 @@ struct MigrateToDevice: View { } struct OnionView: View { + @EnvironmentObject var theme: AppTheme @State var appSettings: AppSettings @State private var onionHosts: OnionHosts = .no var finishMigration: (AppSettings) -> Void @@ -380,18 +442,20 @@ struct MigrateToDevice: View { appSettings.networkConfig = updated finishMigration(appSettings) }) { - settingsRow("checkmark") { - Text("Apply").foregroundColor(.accentColor) + 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("Network settings") { + Section(header: Text("Network settings").foregroundColor(theme.colors.secondary)) { Picker("Use .onion hosts", selection: $onionHosts) { ForEach(OnionHosts.values, id: \.self) { Text($0.text) } } @@ -432,10 +496,10 @@ struct MigrateToDevice: View { chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in await MainActor.run { switch msg { - case let .rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer): + 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 .rcvStandaloneFileComplete: + case .result(.rcvStandaloneFileComplete): DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // User closed the whole screen before new state was saved if migrationState == nil { @@ -445,10 +509,10 @@ struct MigrateToDevice: View { MigrationToDeviceState.save(.archiveImport(archiveName: URL(fileURLWithPath: archivePath).lastPathComponent)) } } - case .rcvFileError: + 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 .chatError(_, .error(.noRcvFileUser)): + 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: @@ -473,8 +537,12 @@ struct MigrateToDevice: View { 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) @@ -541,11 +609,22 @@ struct MigrateToDevice: View { do { try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory()) MigrationToDeviceState.save(nil) - appSettings.importIntoApp() - try SimpleX.startChat(refreshInvitations: true) - AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.") + 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 { - AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(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() } @@ -553,6 +632,8 @@ struct MigrateToDevice: View { private func hideView() { onboardingStageDefault.set(.onboardingComplete) m.onboardingStage = .onboardingComplete + m.migrationState = nil + MigrationToDeviceState.save(nil) dismiss() } @@ -566,6 +647,7 @@ struct MigrateToDevice: View { } private struct PassphraseEnteringView: View { + @EnvironmentObject var theme: AppTheme @Binding var migrationState: MigrationToState? @State private var useKeychain = true @State var currentKey: String @@ -577,7 +659,7 @@ private struct PassphraseEnteringView: View { ZStack { List { Section { - settingsRow("key", color: .secondary) { + settingsRow("key", color: theme.colors.secondary) { Toggle("Save passphrase in Keychain", isOn: $useKeychain) } @@ -606,13 +688,14 @@ private struct PassphraseEnteringView: View { verifyingPassphrase = false } }) { - settingsRow("key", color: .secondary) { + 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 { @@ -623,6 +706,7 @@ private struct PassphraseEnteringView: View { Text("**Warning**: Instant push notifications require passphrase saved in Keychain.") } } + .foregroundColor(theme.colors.secondary) .font(.callout) .padding(.top, 1) .onTapGesture { keyboardVisible = false } @@ -667,11 +751,11 @@ private func progressView() -> some View { private class MigrationChatReceiver { let ctrl: chat_ctrl let databaseUrl: URL - let processReceivedMsg: (ChatResponse) async -> Void + let processReceivedMsg: (APIResult) async -> Void private var receiveLoop: Task? private var receiveMessages = true - init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatResponse) async -> Void) { + init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (APIResult) async -> Void) { self.ctrl = ctrl self.databaseUrl = databaseUrl self.processReceivedMsg = processReceivedMsg @@ -688,7 +772,7 @@ private class MigrationChatReceiver { // TODO use function that has timeout if let msg = await chatRecvMsg(ctrl) { Task { - await TerminalItems.shared.add(.resp(.now, msg)) + await TerminalItems.shared.addResult(msg) } logger.debug("processReceivedMsg: \(msg.responseType)") await processReceivedMsg(msg) diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift index 45eb783326..3a64a955c5 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift @@ -28,8 +28,11 @@ struct AddContactLearnMore: View { 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)) } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 4b272f4caa..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,43 +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, size: 128, color: Color(uiColor: .secondarySystemGroupedBackground)) + ProfileImage(imageStr: profile.image, size: 128) if profile.image != nil { Button { profile.image = nil @@ -95,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) @@ -104,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) } @@ -138,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 { @@ -156,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) @@ -188,10 +191,7 @@ struct AddGroupView: View { 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) @@ -200,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/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index c3452ce18d..e5263813fa 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -7,46 +7,491 @@ // import SwiftUI +import SimpleXChat -enum NewChatMenuOption: Identifiable { - case newContact - case newGroup - - var id: Self { self } +enum ContactType: Int { + case card, request, recent, chatDeleted, unlisted } struct NewChatMenuButton: View { - @Binding var newChatMenuOption: NewChatMenuOption? + // 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 { - Menu { Button { - newChatMenuOption = .newContact - } label: { - Text("Add contact") - } - Button { - newChatMenuOption = .newGroup - } label: { - Text("Create group") - } + showNewChatSheet = true } label: { Image(systemName: "square.and.pencil") .resizable() .scaledToFit() .frame(width: 24, height: 24) } - .sheet(item: $newChatMenuOption) { opt in - switch opt { - case .newContact: NewChatView(selection: .invite) - case .newGroup: AddGroupView() - } + .appSheet(isPresented: $showNewChatSheet) { + NewChatSheet() + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } + .alert(item: $alert) { a in + return a.alert } } } -#Preview { - NewChatMenuButton( - newChatMenuOption: Binding.constant(nil) - ) +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(_ 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(_ 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 index 7ece4fdee6..110eda7882 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -10,21 +10,27 @@ import SwiftUI import SimpleXChat import CodeScanner import AVFoundation +import SimpleXChat -enum SomeAlert: Identifiable { - case someAlert(alert: Alert, id: String) +struct SomeAlert: Identifiable { + var alert: Alert + var id: String +} - var id: String { - switch self { - case let .someAlert(_, id): return id - } - } +struct SomeActionSheet: Identifiable { + var actionSheet: ActionSheet + var id: String +} + +struct SomeSheet: 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)" @@ -40,40 +46,64 @@ enum NewChatOption: Identifiable { 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 contactConnection: PendingContactConnection? = nil - @State private var connReqInvitation: String = "" + @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) { - HStack { - Text("New chat") - .font(.largeTitle) - .bold() - .fixedSize(horizontal: false, vertical: true) - Spacer() - InfoSheetButton { - AddContactLearnMore(showTitle: true) - } - } - .padding() - .padding(.top) - Picker("New chat", selection: $selection) { - Label("Add contact", systemImage: "link") + 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 @@ -91,10 +121,11 @@ struct NewChatView: View { } } .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(Color(uiColor: .systemGroupedBackground)) + .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) @@ -113,48 +144,44 @@ struct NewChatView: View { } ) } - .background(Color(.systemGroupedBackground)) + .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 !(m.showingInvitation?.connChatUsed ?? true), - let conn = contactConnection { - AlertManager.shared.showAlert(Alert( - title: Text("Keep unused invitation?"), - message: Text("You can view invitation link again in connection details."), - primaryButton: .default(Text("Keep")) {}, - secondaryButton: .destructive(Text("Delete")) { - Task { - await deleteChat(Chat( - chatInfo: .contactConnection(contactConnection: conn), - chatItems: [] - )) - } - } - )) + if !choosingProfile { + showKeepInvitationAlert() + contactConnection = nil } - m.showingInvitation = nil } .alert(item: $alert) { a in switch(a) { case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" }) - case let .newChatSomeAlert(.someAlert(alert, _)): - return alert + 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 connReqInvitation != "" { + if connLinkInvitation.connFullLink != "" { InviteView( invitationUsed: $invitationUsed, contactConnection: $contactConnection, - connReqInvitation: connReqInvitation + connLinkInvitation: $connLinkInvitation, + showShortLink: $showShortLink, + choosingProfile: $choosingProfile ) } else if creatingConnReq { creatingLinkProgressView() @@ -165,23 +192,23 @@ struct NewChatView: View { } private func createInvitation() { - if connReqInvitation == "" && contactConnection == nil && !creatingConnReq { + 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 (connReq, pcc) = r { + if let (connLink, pcc) = r { await MainActor.run { m.updateContactConnection(pcc) - m.showingInvitation = ShowingInvitation(connId: pcc.id, connChatUsed: false) - connReqInvitation = connReq + 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")) + alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error")) } } } @@ -205,49 +232,74 @@ struct NewChatView: View { } } +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? - var connReqInvitation: String + @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("Share this 1-time invite link") { + 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() - - Section { - IncognitoToggle(incognitoEnabled: $incognitoDefault) - } footer: { - sharedProfileInfo(incognitoDefault) + 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 - 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))") - } - } + setInvitationUsed() + } + .onChange(of: chatModel.currentUser) { u in setInvitationUsed() } } private func shareLinkView() -> some View { HStack { - let link = simplexChatLink(connReqInvitation) + let link = connLinkInvitation.simplexChatUri(short: showShortLink) linkTextView(link) Button { showShareSheet(items: [link]) @@ -261,8 +313,9 @@ private struct InviteView: View { } private func qrCodeView() -> some View { - Section("Or show this code") { - SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed) + Section { + SimpleXCreatedLinkQRCode(link: connLinkInvitation, short: $showShortLink, onShare: setInvitationUsed) + .id("simplex-qrcode-view-for-\(connLinkInvitation.simplexChatUri(short: showShortLink))") .padding() .background( RoundedRectangle(cornerRadius: 12, style: .continuous) @@ -272,6 +325,8 @@ private struct InviteView: View { .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) } } @@ -282,19 +337,271 @@ private struct InviteView: View { } } +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("Paste the link you received") { + Section(header: Text("Paste the link you received").foregroundColor(theme.colors.secondary)) { pasteLinkView() } - Section("Or scan QR code") { + Section(header: Text("Or scan QR code").foregroundColor(theme.colors.secondary)) { ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode) } } @@ -315,7 +622,7 @@ private struct ConnectView: View { // showQRCodeScanner = false connect(pastedLink) } else { - alert = .newChatSomeAlert(alert: .someAlert( + 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" )) @@ -324,7 +631,7 @@ private struct ConnectView: View { } label: { Text("Tap to paste link") } - .disabled(!ChatModel.shared.pasteboardHasStrings) + .disabled(!pasteboardHasStrings) .frame(maxWidth: .infinity, alignment: .center) } else { linkTextView(pastedLink) @@ -338,14 +645,14 @@ private struct ConnectView: View { if strIsSimplexLink(r.string) { connect(link) } else { - alert = .newChatSomeAlert(alert: .someAlert( + 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 = .newChatSomeAlert(alert: SomeAlert( alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"), id: "processQRCode: failure" )) @@ -367,11 +674,12 @@ struct ScannerInView: View { @Binding var showQRCodeScanner: Bool let processQRCode: (_ resp: Result) -> Void @State private var cameraAuthorizationStatus: AVAuthorizationStatus? + var scanMode: ScanMode = .continuous var body: some View { Group { if showQRCodeScanner, case .authorized = cameraAuthorizationStatus { - CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode) + CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) .listRowBackground(Color.clear) @@ -394,6 +702,7 @@ struct ScannerInView: View { .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") @@ -413,21 +722,26 @@ struct ScannerInView: View { .disabled(cameraAuthorizationStatus == .restricted) } } - .onAppear { + .task { let status = AVCaptureDevice.authorizationStatus(for: .video) cameraAuthorizationStatus = status if showQRCodeScanner { switch status { - case .notDetermined: askCameraAuthorization() + case .notDetermined: await askCameraAuthorizationAsync() case .restricted: showQRCodeScanner = false case .denied: showQRCodeScanner = false case .authorized: () - @unknown default: askCameraAuthorization() + @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) @@ -436,6 +750,7 @@ struct ScannerInView: View { } } + private func linkTextView(_ link: String) -> some View { Text(link) .lineLimit(1) @@ -486,6 +801,7 @@ func strHasSingleSimplexLink(_ str: String) -> FormattedText? { } struct IncognitoToggle: View { + @EnvironmentObject var theme: AppTheme @Binding var incognitoEnabled: Bool @State private var showIncognitoSheet = false @@ -493,13 +809,13 @@ struct IncognitoToggle: View { ZStack(alignment: .leading) { Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks") .frame(maxWidth: 24, maxHeight: 24, alignment: .center) - .foregroundColor(incognitoEnabled ? Color.indigo : .secondary) + .foregroundColor(incognitoEnabled ? Color.indigo : theme.colors.secondary) .font(.system(size: 14)) Toggle(isOn: $incognitoEnabled) { HStack(spacing: 6) { Text("Incognito") Image(systemName: "info.circle") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .font(.system(size: 14)) } .onTapGesture { @@ -524,23 +840,25 @@ func sharedProfileInfo(_ incognito: Bool) -> Text { } 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?) + 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)" - 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)" + 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)" } } } @@ -605,11 +923,17 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: ( ) case let .groupLinkConnecting(_, groupInfo): if let groupInfo = groupInfo { - return Alert( + 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!"), @@ -617,21 +941,22 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: ( dismissButton: .default(Text("OK")) { cleanup?() } ) } + case let .error(_, alert): return alert } } enum PlanAndConnectActionSheet: Identifiable { - case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey) + case askCurrentOrIncognitoProfile(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) + case askCurrentOrIncognitoProfileDestructive(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, title: LocalizedStringKey) case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact) - case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) + case ownGroupLinkConfirmConnect(connectionLink: CreatedConnLink, 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 .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)" + case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink.connFullLink)" } } } @@ -690,7 +1015,7 @@ func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool } func planAndConnect( - _ connectionLink: String, + _ shortOrFullLink: String, showAlert: @escaping (PlanAndConnectAlert) -> Void, showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void, dismiss: Bool, @@ -700,8 +1025,8 @@ func planAndConnect( filterKnownGroup: ((GroupInfo) -> Void)? = nil ) { Task { - do { - let connectionPlan = try await apiConnectPlan(connReq: connectionLink) + let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink) + if let (connectionLink, connectionPlan) = result { switch connectionPlan { case let .invitationLink(ilp): switch ilp { @@ -710,32 +1035,40 @@ func planAndConnect( if let incognito = incognito { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) + 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")") - 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!")) + 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")") - if let contact = contact_ { - if let f = filterKnownContact { - f(contact) + await MainActor.run { + if let contact = contact_ { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + } } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) } - } else { - showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) } case let .known(contact): logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownContact { - f(contact) - } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + } } } case let .contactAddress(cap): @@ -745,83 +1078,109 @@ func planAndConnect( if let incognito = incognito { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) + 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")") - 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!")) + 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")") - 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?")) + 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")") - if let f = filterKnownContact { - f(contact) - } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + 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")") - if let f = filterKnownContact { - f(contact) - } else { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + 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 { - showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact)) + await MainActor.run { + 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")) + 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")") - if let f = filterKnownGroup { - f(groupInfo) + await MainActor.run { + if let f = filterKnownGroup { + f(groupInfo) + } + showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) } - 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?")) + 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")") - showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) + await MainActor.run { + showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) + } case let .known(groupInfo): logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") - if let f = filterKnownGroup { - f(groupInfo) - } else { - openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) } + 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")) + } } - } catch { - logger.debug("planAndConnect, plan error") - 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)) } } } @@ -834,28 +1193,31 @@ private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incogn dismissAllSheets(animated: true) } } - _ = await connectContactViaAddress(contact.contactId, incognito) + let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { AlertManager.shared.showAlert($0) }) + if ok { + AlertManager.shared.showAlert(connReqSentAlert(.contact)) + } cleanup?() } } private func connectViaLink( - _ connectionLink: String, + _ connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool, cleanup: (() -> Void)? ) { Task { - if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) { + if let (connReqType, pcc) = await apiConnect(incognito: incognito, connLink: connectionLink) { await MainActor.run { ChatModel.shared.updateContactConnection(pcc) } let crt: ConnReqType - if let plan = connectionPlan { - crt = planToConnReqType(plan) + crt = if let plan = connectionPlan { + planToConnReqType(plan) ?? connReqType } else { - crt = connReqType + connReqType } DispatchQueue.main.async { if dismiss { @@ -878,39 +1240,35 @@ private func connectViaLink( } 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 + 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)?) { - 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 + 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?() + } } } } @@ -923,10 +1281,15 @@ func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert { } func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert { - mkAlert( + 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 { @@ -943,11 +1306,12 @@ enum ConnReqType: Equatable { } } -private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType { +private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType? { switch connectionPlan { - case .invitationLink: return .invitation - case .contactAddress: return .contact - case .groupLink: return .groupLink + case .invitationLink: .invitation + case .contactAddress: .contact + case .groupLink: .groupLink + case .error: nil } } @@ -958,8 +1322,13 @@ func connReqSentAlert(_ type: ConnReqType) -> Alert { ) } -#Preview { - NewChatView( - selection: .invite - ) +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/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index e3bae9287a..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,6 +21,16 @@ 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 @@ -31,12 +42,6 @@ struct SimpleXLinkQRCode: View { } } -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 @@ -49,34 +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)]) - onShare?() + .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) } } @@ -89,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/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() + @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.. + @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 0ee6baa765..ae72cb1be5 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -27,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? @@ -37,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") } @@ -45,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 { @@ -59,154 +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 { - 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_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) - } - case .chatCmdError(_, .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)) - } + 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 { 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("""

Hi!

Connect to me via SimpleX Chat

- """, 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 94e281be7d..9f41a37b1d 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -13,59 +13,62 @@ 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() + + 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 { - OnboardingActionButton() - Spacer() + VStack(spacing: 10) { + createFirstProfileButton() - Button { - m.migrationState = .pasteOrScanLink - } label: { - Label("Migrate from another device", systemImage: "tray.and.arrow.down") - .font(.subheadline) + 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) } - - Button { - showHowItWorks = true - } label: { - Label("How it works", systemImage: "info.circle") - .font(.subheadline) - } - .padding(.bottom, 8) - .frame(maxWidth: .infinity) - } + .padding(.horizontal, 25) + .padding(.top, 75) + .padding(.bottom, 25) .frame(minHeight: g.size.height) } .sheet(isPresented: Binding( @@ -79,80 +82,72 @@ struct SimpleXInfo: View { MigrateToDevice(migrationState: $m.migrationState) } .navigationTitle("Migrate here") - .background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear) + .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 - @Environment(\.colorScheme) var colorScheme +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) - } - - private func actionButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View { - Button { - withAnimation { - action() - } - } 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 209c440b16..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,214 +219,366 @@ 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: [ - FeatureDescription( + .feature(Description( icon: "folder", title: "Private notes", description: "With encrypted files and media." - ), - FeatureDescription( + )), + .feature(Description( icon: "link", title: "Paste link to connect!", description: "Search bar accepts invitation links." - ), - FeatureDescription( + )), + .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)." - ), - FeatureDescription( + )), + .feature(Description( icon: "battery.50", title: "Improved message delivery", description: "With reduced battery usage." - ), - FeatureDescription( + )), + .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: [ - FeatureDescription( + .feature(Description( icon: "key", title: "Quantum resistant encryption", description: "Enable in direct chats (BETA)!" - ), - FeatureDescription( + )), + .feature(Description( icon: "tray.and.arrow.up", title: "App data migration", description: "Migrate to another device via QR code." - ), - FeatureDescription( + )), + .feature(Description( icon: "phone", title: "Picture-in-picture calls", description: "Use the app while in the call." - ), - FeatureDescription( + )), + .feature(Description( icon: "hand.raised", title: "Safer groups", description: "Admins can block a member for all." - ), - FeatureDescription( + )), + .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: [ - FeatureDescription( + .feature(Description( icon: "key", title: "Quantum resistant encryption", description: "Will be enabled in direct chats!" - ), - FeatureDescription( + )), + .feature(Description( icon: "arrowshape.turn.up.forward", title: "Forward and save messages", description: "Message source remains private." - ), - FeatureDescription( + )), + .feature(Description( icon: "music.note", title: "In-call sounds", description: "When connecting audio and video calls." - ), - FeatureDescription( + )), + .feature(Description( icon: "person.crop.square", title: "Shape profile images", description: "Square, circle, or anything in between." - ), - FeatureDescription( + )), + .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.") + ] + )), + ] + ), ] private let lastVersion = versionDescriptions.last!.version @@ -421,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) } } @@ -472,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) + } + } + } } } @@ -521,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 3059b049a3..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", disabled: Binding.constant(false)) { - 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 = 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) in + let d = sendSimpleXCmdStr(cmd) + Task { + guard let d else { + await TerminalItems.shared.addCommand(start, ChatCommand.string(cmd), APIResult.error(.invalidJSON(json: nil))) + return + } + let r0: APIResult = decodeAPIResult(d) + guard case .invalid = r0 else { + await TerminalItems.shared.addCommand(start, .string(cmd), r0) + return + } + let r1: APIResult = decodeAPIResult(d) + guard case .invalid = r1 else { + await TerminalItems.shared.addCommand(start, .string(cmd), r1) + return + } + let r2: APIResult = 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.invalid(type: "contactSubscribed", json: "{}".data(using: .utf8)!).unexpected), + .err(.now, APIResult.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 f027127db3..0000000000 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ /dev/null @@ -1,171 +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: [10_000000, 15_000000, 20_000000, 25_000000, 35_000000, 50_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(.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, 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, 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 index ba192b333c..44e0b20958 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -19,18 +19,29 @@ extension AppSettings { val.hostMode = .publicHost val.requiredHostMode = true } - val.socksProxy = nil - setNetCfg(val) + 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 { def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) } + 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) } @@ -43,18 +54,33 @@ extension AppSettings { 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) @@ -67,6 +93,15 @@ extension AppSettings { 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 b91d2c9369..c6d0e27289 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -7,12 +7,17 @@ // 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)! @@ -20,11 +25,33 @@ 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{ @@ -39,15 +66,130 @@ struct AppearanceSettings: View { } } - Section("App icon") { - HStack { - updateAppIcon(image: "icon-light", icon: nil, tapped: $iconLightTapped) - Spacer().frame(width: 16) - updateAppIcon(image: "icon-dark", icon: "DarkAppIcon", tapped: $iconDarkTapped) + 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("Profile images") { + 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) @@ -61,37 +203,91 @@ struct AppearanceSettings: View { step: 2.5 ) } - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } - Section { - Picker("Theme", selection: $userInterfaceStyle) { - ForEach(interfaceStyles, id: \.self) { style in - Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0]) - } + 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) } - .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 { @@ -117,6 +313,785 @@ struct AppearanceSettings: View { } } +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 = { 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 + + 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 { + 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 + 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 + // 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) { + 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 + + 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( @@ -127,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) { + 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(_ 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 @@ -144,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 3bbfbfe33e..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,28 +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") + } } } } } + + 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 fc478596a9..d9862aaac8 100644 --- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -26,7 +26,9 @@ struct IncognitoHelp: View { 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)) } + .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 a6702b1821..0000000000 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ /dev/null @@ -1,191 +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 { - @EnvironmentObject var m: ChatModel - @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") - } - } - - Section("Network connection") { - HStack { - Text(m.networkInfo.networkType.text) - Spacer() - Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red) - } - } - } - } - .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, 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, 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 = """ + + """ + let head = "\(styles)" + 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 { + 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.. 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 65% rename from apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift index 6702ab7ce8..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", disabled: Binding.constant(false)) { - 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 @@ -137,7 +163,7 @@ struct BackButton: ViewModifier { } } -@ViewBuilder func showTestStatus(server: ServerCfg) -> some View { +@ViewBuilder func showTestStatus(server: UserServer) -> some View { switch server.tested { case .some(true): Image(systemName: "checkmark") @@ -150,7 +176,7 @@ struct BackButton: ViewModifier { } } -func testServerConnection(server: Binding) async -> ProtocolTestFailure? { +func testServerConnection(server: Binding) async -> ProtocolTestFailure? { do { let r = try await testProtoServer(server: server.wrappedValue.server) switch r { @@ -170,16 +196,14 @@ func testServerConnection(server: Binding) 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 + @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.. [String: ProtocolTestFailure] { + var fs: [String: ProtocolTestFailure] = [:] + for i in 0..) { 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 4876d60eca..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 { @@ -78,13 +94,16 @@ struct NotificationsView: View { } if let server = m.notificationServer { - smpServers("Push server", [server]) + 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) } @@ -109,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)) } @@ -133,12 +157,13 @@ struct NotificationsView: View { 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: @@ -150,28 +175,123 @@ struct NotificationsView: View { 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: View { + @EnvironmentObject var theme: AppTheme var list: [Item] @Binding var selection: Item var onSelection: ((Item) -> Void)? @@ -179,32 +299,24 @@ struct SelectionListView: 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)) } @@ -212,11 +324,13 @@ struct SelectionListView: 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 2e560f8578..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) -> 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,11 +66,11 @@ 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) -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 8d13c6fb39..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 diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift deleted file mode 100644 index b9163d4bad..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(disabled: Binding.constant(false)) { - 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) -> 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.. [String: ProtocolTestFailure] { - var fs: [String: ProtocolTestFailure] = [:] - for i in 0..(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 { + 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 = 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 { @@ -163,187 +277,158 @@ struct SettingsView: View { if showProgress { progressView() } - if let la = chatModel.laRequest { - LocalAuthView(authRequest: la) - } } } - @ViewBuilder func settingsView() -> some View { - let user = chatModel.currentUser - NavigationView { - List { - Section("You") { - if let user = user { - NavigationLink { - UserProfile() - .navigationTitle("Your current profile") - } label: { - ProfilePreview(profileOf: user) - .padding(.leading, -8) - } - } - - NavigationLink { - UserProfilesView(showSettings: $showSettings) - } label: { - settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") } - } - - - if let user = user { - NavigationLink { - UserAddressView(shareViaProfile: user.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") } - } - - NavigationLink { - MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress) - .navigationTitle("Migrate device") - .navigationBarTitleDisplayMode(.large) - } label: { - settingsRow("tray.and.arrow.up") { Text("Migrate to another device") } + 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() + 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) - Section("Help") { - if let user = user { - NavigationLink { - ChatHelp(showSettings: $showSettings) - .navigationTitle("Welcome \(user.displayName)!") - .frame(maxHeight: .infinity, alignment: .top) - } label: { - settingsRow("questionmark") { Text("How to use it") } - } - } + 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 { - WhatsNewView(viaSettings: true) - .navigationBarTitleDisplayMode(.inline) + AppearanceSettings() + .navigationTitle("Appearance") + .modifier(ThemedBackground(grouped: true)) } 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 = [] @@ -352,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") @@ -386,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 @@ -401,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) @@ -412,7 +502,7 @@ struct SettingsView: View { } } -func settingsRow(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View { +func settingsRow(_ 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) @@ -423,32 +513,31 @@ func settingsRow(_ 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, 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 96eeffd16d..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? = 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(disabled: Binding.constant(false)) { - 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, _ 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, _ savedAAS: Binding) { + 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 198fd495bd..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) -> 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,6 +180,13 @@ struct UserProfile: View { } } } + + private func getCurrentProfile() { + if let user = chatModel.currentUser { + profile = fromLocalProfile(user.profile) + currentProfileHash = profile.hashValue + } + } } func profileImageView(_ imageStr: String?) -> some View { @@ -201,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 8c1a3bf4e1..887023b670 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -8,7 +8,7 @@ import SimpleXChat struct UserProfilesView: View { @EnvironmentObject private var m: ChatModel - @Binding var showSettings: Bool + @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 @@ -21,6 +21,7 @@ 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)} @@ -29,7 +30,7 @@ struct UserProfilesView: View { 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 { @@ -55,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 { @@ -77,12 +67,14 @@ struct UserProfilesView: View { Section { let users = filteredUsers() let v = ForEach(users) { u in - userView(u.user) + 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 { @@ -90,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) @@ -111,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) @@ -123,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 @@ -131,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 @@ -169,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) } } } @@ -188,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 @@ -200,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) } } @@ -227,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 { @@ -252,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) + } } } } @@ -269,6 +298,7 @@ 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() @@ -280,7 +310,7 @@ struct UserProfilesView: View { await MainActor.run { onboardingStageDefault.set(.step1_SimpleXInfo) m.onboardingStage = .step1_SimpleXInfo - showSettings = false + dismissAllSheets() } } } else { @@ -294,69 +324,86 @@ struct UserProfilesView: View { 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) -> 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, size: 44, color: Color(uiColor: .tertiarySystemFill)) - .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) + } + } } } } - .foregroundColor(.primary) + .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, *) { @@ -364,7 +411,9 @@ struct UserProfilesView: View { } else { v.swipeActions(edge: .trailing, allowsFullSwipe: true) { Button("Delete", role: .destructive) { - confirmDeleteUser(user) + withAuth { + confirmDeleteUser(user) + } } } } @@ -401,8 +450,15 @@ 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(showSettings: Binding.constant(true)) + 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 @@ applinks:simplex.chat applinks:www.simplex.chat applinks:simplex.chat?mode=developer + applinks:*.simplex.im + applinks:*.simplex.im?mode=developer + applinks:*.simplexonflux.com + applinks:*.simplexonflux.com?mode=developer com.apple.security.application-groups 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 @@ !1 colored! - ! 1 ملون! + ! 1 مُلوَّن! No comment provided by engineer. @@ -49,7 +49,7 @@ %@ - %@ + %@ No comment provided by engineer. @@ -69,7 +69,7 @@ %@ is not verified - %@ لم يتم التحقق منها + %@ لم يتم التحقق منه No comment provided by engineer. @@ -107,8 +107,9 @@ %d ثانية message ttl - + %d skipped message(s) + %d الرسائل المتخطية integrity error chat item @@ -121,12 +122,14 @@ %lld %@ No comment provided by engineer. - + %lld contact(s) selected + %lld تم اختيار جهات الاتصال No comment provided by engineer. - + %lld file(s) with total size of %@ + %lld الملفات ذات الحجم الإجمالي %@ No comment provided by engineer. @@ -134,8 +137,9 @@ %lld أعضاء No comment provided by engineer. - + %lld second(s) + %lld ثوانى No comment provided by engineer. @@ -183,24 +187,19 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - ** إضافة جهة اتصال جديدة **: لإنشاء رمز QR لمرة واحدة أو رابط جهة الاتصال الخاصة بك. - No comment provided by engineer. - **Create link / QR code** for your contact to use. ** أنشئ رابطًا / رمز QR ** لتستخدمه جهة الاتصال الخاصة بك. 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. @@ -210,11 +209,11 @@ **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. + + **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. @@ -305,7 +304,7 @@ A separate TCP connection will be used **for each chat profile you have in the app**. - سيتم استخدام اتصال TCP منفصل ** لكل ملف تعريف دردشة لديك في التطبيق **. + سيتم استخدام اتصال TCP منفصل ** لكل ملف تعريف دردشة لديكم في التطبيق **. No comment provided by engineer. @@ -355,312 +354,381 @@ Accept requests No comment provided by engineer. - + Add preset servers + إضافة خوادم محددة مسبقا No comment provided by engineer. - + Add profile + إضافة الملف الشخصي No comment provided by engineer. - + Add servers by scanning QR codes. + إضافة خوادم عن طريق مسح رموز QR. No comment provided by engineer. - - Add server… + + Add server + أضف الخادم No comment provided by engineer. - + Add to another device + أضف إلى جهاز آخر 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. - + All chats and messages will be deleted - this cannot be undone! + سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا! No comment provided by engineer. - + All group members will remain connected. + سيبقى جميع أعضاء المجموعة على اتصال. No comment provided by engineer. - + All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. + سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك. No comment provided by engineer. All your contacts will remain connected No comment provided by engineer. - + Allow + سماح No comment provided by engineer. - + Allow disappearing messages only if your contact allows it to you. + السماح بالرسائل المختفية فقط إذا سمحت لك جهة الاتصال بذلك. No comment provided by engineer. Allow irreversible message deletion only if your contact allows it to you. No comment provided by engineer. - + Allow sending direct messages to members. + السماح بإرسال رسائل مباشرة إلى الأعضاء. No comment provided by engineer. - + Allow sending disappearing messages. + السماح بإرسال الرسائل التي تختفي. No comment provided by engineer. Allow to irreversibly delete sent messages. No comment provided by engineer. - + Allow to send voice messages. + السماح بإرسال رسائل صوتية. No comment provided by engineer. - + Allow voice messages only if your contact allows them. + اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك. No comment provided by engineer. - + Allow voice messages? + السماح بالرسائل الصوتية؟ No comment provided by engineer. Allow your contacts to irreversibly delete sent messages. No comment provided by engineer. - + Allow your contacts to send disappearing messages. + السماح لجهات اتصالك بإرسال رسائل تختفي. No comment provided by engineer. - + Allow your contacts to send voice messages. + اسمح لجهات اتصالك بإرسال رسائل صوتية. No comment provided by engineer. - + Already connected? + متصل بالفعل؟ No comment provided by engineer. - + Answer call + أجب الاتصال No comment provided by engineer. - + App build: %@ + إصدار التطبيق: %@ No comment provided by engineer. - + App icon + رمز التطبيق No comment provided by engineer. - + App version + نسخة التطبيق No comment provided by engineer. - + App version: v%@ + نسخة التطبيق: v%@ No comment provided by engineer. - + Appearance + المظهر No comment provided by engineer. - + Attach + إرفاق No comment provided by engineer. - + Audio & video calls + مكالمات الصوت والفيديو No comment provided by engineer. - + Authentication failed + فشلت المصادقة No comment provided by engineer. - + Authentication unavailable + المصادقة غير متاحة No comment provided by engineer. - + Auto-accept contact requests + قبول طلبات الاتصال تلقائيًا No comment provided by engineer. - + Auto-accept images + قبول تلقائي للصور No comment provided by engineer. Automatically No comment provided by engineer. - + Back + رجوع No comment provided by engineer. Both you and your contact can irreversibly delete sent messages. No comment provided by engineer. - + Both you and your contact can send disappearing messages. + يمكنك أنت وجهة اتصالك إرسال رسائل تختفي. No comment provided by engineer. - + Both you and your contact can send voice messages. + يمكنك أنت وجهة اتصالك إرسال رسائل صوتية. No comment provided by engineer. - + By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). + حسب ملف تعريف الدردشة (افتراضي) أو [حسب الاتصال] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - + Call already ended! + انتهت المكالمة بالفعل! No comment provided by engineer. - + Calls + المكالمات No comment provided by engineer. - + Can't invite contact! + لا يمكن دعوة جهة اتصال! No comment provided by engineer. - + Can't invite contacts! + لا يمكن دعوة جهات الاتصال! No comment provided by engineer. - + Cancel + إلغاء No comment provided by engineer. - + Cannot access keychain to save database password + لا يمكن الوصول إلى سلسلة المفاتيح لحفظ كلمة مرور قاعدة البيانات No comment provided by engineer. - + Cannot receive file + لا يمكن استلام الملف No comment provided by engineer. - + Change + تغير No comment provided by engineer. - + Change database passphrase? + تغيير عبارة مرور قاعدة البيانات؟ No comment provided by engineer. - + Change member role? + تغيير دور العضو؟ No comment provided by engineer. - + Change receiving address + تغيير عنوان الاستلام No comment provided by engineer. - + Change receiving address? + تغيير عنوان الاستلام؟ No comment provided by engineer. - + Change role + تغيير الدور No comment provided by engineer. Chat archive No comment provided by engineer. - + Chat console + وحدة تحكم الدردشة No comment provided by engineer. - + Chat database + قاعدة بيانات الدردشة No comment provided by engineer. - + Chat database deleted + حُذفت قاعدة بيانات الدردشة No comment provided by engineer. - + Chat database imported + استُوردت قاعدة بيانات الدردشة No comment provided by engineer. - + Chat is running + الدردشة قيد التشغيل No comment provided by engineer. - + Chat is stopped + توقفت الدردشة No comment provided by engineer. - + Chat preferences + تفضيلات الدردشة No comment provided by engineer. - + Chats + الدردشات No comment provided by engineer. - + Check server address and try again. + تحقق من عنوان الخادم وحاول مرة أخرى. No comment provided by engineer. - + Choose file + اختر الملف No comment provided by engineer. - + Choose from library + اختر من المكتبة No comment provided by engineer. - + Clear + مسح No comment provided by engineer. - + Clear conversation + مسح الدردشة No comment provided by engineer. - + Clear conversation? + مسح الدردشة؟ No comment provided by engineer. - + Clear verification + امسح التحقُّق No comment provided by engineer. Colors No comment provided by engineer. - + Compare security codes with your contacts. + قارن رموز الأمان مع جهات اتصالك. No comment provided by engineer. - + Configure ICE servers + ضبط خوادم ICE No comment provided by engineer. - + Confirm + تأكيد No comment provided by engineer. - + Confirm new passphrase… + تأكيد عبارة المرور الجديدة… No comment provided by engineer. - + Connect + اتصل server test step @@ -671,8 +739,9 @@ Connect via group link? No comment provided by engineer. - + Connect via link + تواصل عبر الرابط No comment provided by engineer. @@ -687,224 +756,273 @@ Connect via relay No comment provided by engineer. - + Connecting to server… + جارِ الاتصال بالخادم… No comment provided by engineer. - + Connecting to server… (error: %@) + الاتصال بالخادم... (الخطأ: %@) No comment provided by engineer. - + Connection + الاتصال No comment provided by engineer. - + Connection error + خطأ في الإتصال No comment provided by engineer. - + Connection error (AUTH) + خطأ في الإتصال (المصادقة) No comment provided by engineer. Connection request No comment provided by engineer. - + Connection request sent! + أرسلت طلب الاتصال! No comment provided by engineer. - + Connection timeout + انتهت مهلة الاتصال No comment provided by engineer. - + Contact allows + تسمح جهة الاتصال No comment provided by engineer. - + Contact already exists + جهة الاتصال موجودة بالفعل No comment provided by engineer. Contact and all messages will be deleted - this cannot be undone! No comment provided by engineer. - + Contact hidden: + جهة الاتصال مخفية: notification - + Contact is connected + تم الاتصال notification Contact is not connected yet! No comment provided by engineer. - + Contact name + اسم جهة الاتصال No comment provided by engineer. - + Contact preferences + تفضيلات جهة الاتصال No comment provided by engineer. Contact requests No comment provided by engineer. - + Contacts can mark messages for deletion; you will be able to view them. + يمكن لجهات الاتصال تحديد الرسائل لحذفها؛ ستتمكن من مشاهدتها. No comment provided by engineer. - + Copy + نسخ chat item action Core built at: %@ No comment provided by engineer. - + Core version: v%@ + الإصدار الأساسي: v%@ No comment provided by engineer. - + Create + إنشاء No comment provided by engineer. Create address No comment provided by engineer. - + Create group link + إنشاء رابط المجموعة No comment provided by engineer. - + Create link + إنشاء رابط No comment provided by engineer. Create one-time invitation link No comment provided by engineer. - + Create queue + إنشاء قائمة انتظار server test step - + Create secret group + إنشاء مجموعة سرية No comment provided by engineer. - + Create your profile + أنشئ ملف تعريفك No comment provided by engineer. Created on %@ No comment provided by engineer. - + Current passphrase… + عبارة المرور الحالية… No comment provided by engineer. - + Currently maximum supported file size is %@. + الحد الأقصى لحجم الملف المدعوم حاليًا هو %@. No comment provided by engineer. - + Dark + داكن No comment provided by engineer. - + Database ID + معرّف قاعدة البيانات No comment provided by engineer. - + Database encrypted! + قاعدة البيانات مُعمّاة! No comment provided by engineer. - + Database encryption passphrase will be updated and stored in the keychain. + سيتم تحديث عبارة المرور الخاصة بتشفير قاعدة البيانات وتخزينها في سلسلة المفاتيح. + No comment provided by engineer. - + Database encryption passphrase will be updated. + سيتم تحديث عبارة مرور تعمية قاعدة البيانات. + No comment provided by engineer. - + Database error + خطأ في قاعدة البيانات No comment provided by engineer. - + Database is encrypted using a random passphrase, you can change it. + قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية، يمكنك تغييرها. No comment provided by engineer. - + Database is encrypted using a random passphrase. Please change it before exporting. + قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية. يُرجى تغييره قبل التصدير. No comment provided by engineer. - + Database passphrase + عبارة مرور قاعدة البيانات No comment provided by engineer. - + Database passphrase & export + عبارة مرور قاعدة البيانات وتصديرها No comment provided by engineer. - + Database passphrase is different from saved in the keychain. + عبارة المرور الخاصة بقاعدة البيانات مختلفة عن تلك المحفوظة في سلسلة المفاتيح. No comment provided by engineer. - + Database passphrase is required to open chat. + عبارة مرور قاعدة البيانات مطلوبة لفتح الدردشة. No comment provided by engineer. - + Database will be encrypted and the passphrase stored in the keychain. + سيتم تشفير قاعدة البيانات وتخزين عبارة المرور في سلسلة المفاتيح. + No comment provided by engineer. - + Database will be encrypted. + سيتم تعمية قاعدة البيانات. + No comment provided by engineer. - + Database will be migrated when the app restarts + سيتم نقل قاعدة البيانات عند إعادة تشغيل التطبيق No comment provided by engineer. - + Decentralized + لامركزي No comment provided by engineer. - + Delete + حذف chat item action Delete Contact No comment provided by engineer. - + Delete address + حذف العنوان No comment provided by engineer. - + Delete address? + حذف العنوان؟ No comment provided by engineer. - + Delete after + حذف بعد No comment provided by engineer. - + Delete all files + حذف جميع الملفات No comment provided by engineer. @@ -915,152 +1033,188 @@ Delete chat archive? No comment provided by engineer. - + Delete chat profile? + حذف ملف تعريف الدردشة؟ No comment provided by engineer. - + Delete connection + حذف الاتصال No comment provided by engineer. - + Delete contact + حذف جهة الاتصال No comment provided by engineer. - + Delete contact? + حذف جهة الاتصال؟ No comment provided by engineer. - + Delete database + حذف قاعدة البيانات No comment provided by engineer. - + Delete files and media? + حذف الملفات والوسائط؟ No comment provided by engineer. - + Delete files for all chat profiles + حذف الملفات لجميع ملفات تعريف الدردشة No comment provided by engineer. - + Delete for everyone + حذف للجميع chat feature - + Delete for me + حذف بالنسبة لي No comment provided by engineer. - + Delete group + حذف المجموعة No comment provided by engineer. - + Delete group? + حذف المجموعة؟ No comment provided by engineer. - + Delete invitation + حذف الدعوة No comment provided by engineer. - + Delete link + حذف الرابط No comment provided by engineer. - + Delete link? + حذف الرابط؟ No comment provided by engineer. - + Delete message? + حذف الرسالة؟ No comment provided by engineer. - + Delete messages + حذف الرسائل No comment provided by engineer. - + Delete messages after + حذف الرسائل بعد No comment provided by engineer. - + Delete old database + حذف قاعدة البيانات القديمة No comment provided by engineer. - + Delete old database? + حذف قاعدة البيانات القديمة؟ No comment provided by engineer. Delete pending connection No comment provided by engineer. - + Delete pending connection? + حذف الاتصال قيد الانتظار؟ No comment provided by engineer. - + Delete queue + حذف قائمة الانتظار server test step - + Delete user profile? + حذف ملف تعريف المستخدم؟ No comment provided by engineer. - + Description + الوصف No comment provided by engineer. - + Develop + يطور No comment provided by engineer. - + Developer tools + أدوات المطور No comment provided by engineer. - + Device + الجهاز No comment provided by engineer. - + Device authentication is disabled. Turning off SimpleX Lock. + استيثاق الجهاز مُعطَّل. جارِ إيقاف تشغيل قفل SimpleX. No comment provided by engineer. - + Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. + مصادقة الجهاز غير مفعّلة. يمكنك تشغيل قفل SimpleX عبر الإعدادات، بمجرد تفعيل مصادقة الجهاز. No comment provided by engineer. - + Different names, avatars and transport isolation. + أسماء مختلفة، صور الأفاتار وعزل النقل. No comment provided by engineer. - + Direct messages + رسائل مباشرة chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. + الرسائل المباشرة بين الأعضاء ممنوعة. No comment provided by engineer. - + Disable SimpleX Lock + تعطيل قفل SimpleX authentication reason - + Disappearing messages + الرسائل المختفية chat feature - + 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. - + Disconnect + قطع الاتصال server test step @@ -1071,124 +1225,153 @@ Display name: No comment provided by engineer. - + Do NOT use SimpleX for emergency calls. + لا تستخدم SimpleX لإجراء مكالمات الطوارئ. No comment provided by engineer. - + Do it later + افعل ذلك لاحقا No comment provided by engineer. - + Duplicate display name! + اسم العرض مكرر! No comment provided by engineer. - + Edit + تحرير chat item action - + Edit group profile + حرّر ملف تعريف المجموعة No comment provided by engineer. - + Enable + تفعيل No comment provided by engineer. - + Enable SimpleX Lock + تفعيل قفل SimpleX authentication reason - + Enable TCP keep-alive + تفعيل أبقِ TCP على قيد الحياة No comment provided by engineer. - + Enable automatic message deletion? + تفعيل الحذف التلقائي للرسائل؟ No comment provided by engineer. - + Enable instant notifications? + تفعيل الإشعارات فورية؟ No comment provided by engineer. - + Enable notifications + تفعيل الإشعارات No comment provided by engineer. - + Enable periodic notifications? + تفعيل الإشعارات دورية؟ No comment provided by engineer. - + Encrypt + التشفير No comment provided by engineer. - + Encrypt database? + تشفير قاعدة البيانات؟ No comment provided by engineer. - + Encrypted database + قاعدة بيانات مشفرة No comment provided by engineer. - + Encrypted message or another event + رسالة مشفرة أو حدث آخر notification - + Encrypted message: database error + رسالة مشفرة: خطأ في قاعدة البيانات notification - + Encrypted message: keychain error + رسالة مشفرة: خطأ في سلسلة المفاتيح notification - + Encrypted message: no passphrase + الرسالة المشفرة: لا توجد عبارة مرور notification - + Encrypted message: unexpected error + رسالة مشفرة: خطأ غير متوقع notification - + Enter correct passphrase. + أدخل عبارة المرور الصحيحة. No comment provided by engineer. - + Enter passphrase… + أدخل عبارة المرور… No comment provided by engineer. - + Enter server manually + أدخل الخادم يدوياً No comment provided by engineer. - + Error + خطأ No comment provided by engineer. - + Error accepting contact request + خطأ في قبول طلب الاتصال No comment provided by engineer. Error accessing database file No comment provided by engineer. - + Error adding member(s) + خطأ في إضافة عضو (أعضاء) No comment provided by engineer. - + Error changing address + خطأ في تغيير العنوان No comment provided by engineer. - + Error changing role + خطأ في تغيير الدور المتغير No comment provided by engineer. - + Error changing setting + خطأ في تغيير الإعدادات No comment provided by engineer. @@ -1419,16 +1602,16 @@ Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1519,8 +1702,8 @@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1616,8 +1799,8 @@ 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. @@ -1917,8 +2100,8 @@ We will be adding server redundancy to prevent lost messages. 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**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -1969,8 +2152,9 @@ We will be adding server redundancy to prevent lost messages. Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. + يمكن لأي شخص استضافة الخوادم. No comment provided by engineer. @@ -2001,8 +2185,8 @@ We will be adding server redundancy to prevent lost messages. Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2373,96 +2557,117 @@ We will be adding server redundancy to prevent lost messages. Sent messages will be deleted after set time. No comment provided by engineer. - + Server requires authorization to create queues, check password + يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور server test error - + Server test failed! + فشلت تجربة الخادم! No comment provided by engineer. - + Servers + الخوادم No comment provided by engineer. - + Set 1 day + تعيين يوم واحد No comment provided by engineer. - + Set contact name… + تعيين اسم جهة الاتصال… No comment provided by engineer. - + Set group preferences + عيّن تفضيلات المجموعة No comment provided by engineer. - + Set passphrase to export + عيّن عبارة المرور للتصدير No comment provided by engineer. - + Set timeouts for proxy/VPN + حدد مهلات للوسيط او شبكات افتراضية خاصة (Proxy/VPN timeouts) No comment provided by engineer. - + Settings + الإعدادات No comment provided by engineer. - + Share + مشاركة chat item action Share invitation link No comment provided by engineer. - + Share link + مشاركة الرابط No comment provided by engineer. Share one-time invitation link No comment provided by engineer. - + Show QR code + عرض رمز QR No comment provided by engineer. - + Show preview + عرض المعاينة No comment provided by engineer. - + SimpleX Chat security was audited by Trail of Bits. + تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits. No comment provided by engineer. - + SimpleX Lock + قفل SimpleX No comment provided by engineer. - + SimpleX Lock turned on + تم تشغيل القفل SimpleX No comment provided by engineer. - + SimpleX contact address + عنوان جهة أتصال SimpleX simplex link type - + SimpleX encrypted message or connection event + حَدَثْ SimpleX لرسالة أو اتصال مشفر notification - + SimpleX group link + رابط مجموعة SimpleX simplex link type - + SimpleX links + روابط SimpleX No comment provided by engineer. - + SimpleX one-time invitation + دعوة SimpleX لمرة واحدة simplex link type @@ -2581,8 +2786,8 @@ We will be adding server redundancy to prevent lost messages. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -2613,16 +2818,16 @@ We will be adding server redundancy to prevent lost messages. The microphone does not work when the app is in the background. No comment provided by engineer. - - The next generation of private messaging + + The future of 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. @@ -2677,8 +2882,8 @@ We will be adding server redundancy to prevent lost messages. To prevent the call interruption, enable Do Not Disturb mode. 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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -2851,8 +3056,8 @@ To connect, please ask your contact to create another connection link and check 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. @@ -2963,10 +3168,6 @@ To connect, please ask your contact to create another connection link and check You can use markdown to format messages: 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. @@ -3526,72 +3727,87 @@ SimpleX servers cannot see your profile. secret No comment provided by engineer. - + starting… + يبدأ… No comment provided by engineer. - + strike + شطب No comment provided by engineer. this contact notification title - + unknown + غير معروف connection info - + updated group profile + حدثت ملف تعريف المجموعة rcv group event chat item v%@ (%@) No comment provided by engineer. - + via contact address link + عبر رابط عنوان الاتصال chat list item description - + via group link + عبر رابط المجموعة chat list item description - + via one-time link + عبر رابط لمرة واحدة chat list item description - + via relay + عبر المُرحل No comment provided by engineer. - + video call (not e2e encrypted) + مكالمة الفيديو ليست مُعمّاة بين الطريفين No comment provided by engineer. - + waiting for answer… + بانتظار الرد… No comment provided by engineer. - + waiting for confirmation… + في انتظار التأكيد… No comment provided by engineer. - + wants to connect to you! + يريد الاتصال بك! No comment provided by engineer. - + yes + نعم pref value - + you are invited to group + أنت مدعو إلى المجموعة No comment provided by engineer. - + you changed address + غيّرتَ العنوان chat item text @@ -3606,16 +3822,18 @@ SimpleX servers cannot see your profile. you changed role of %1$@ to %2$@ snd group event chat item - + you left + غادرت snd group event chat item you removed %@ snd group event chat item - + you shared one-time link + لقد شاركت رابط لمرة واحدة chat list item description @@ -3657,7 +3875,7 @@ SimpleX servers cannot see your profile. # %@ - # %@ + # %@ copied message info title, # <title> @@ -3667,7 +3885,7 @@ SimpleX servers cannot see your profile. ## In reply to - ## ردًا على + ## ردًّا على copied message info @@ -3675,6 +3893,1870 @@ SimpleX servers cannot see your profile. %@ و %@ متصل No comment provided by engineer. + + %@ downloaded + %@ تم التنزيل + + + %@ and %@ + %@ و %@ + + + %@ connected + %@ متصل + + + %lld minutes + %lld دقائق + + + %@, %@ and %lld members + %@, %@ و %lld أعضاء + + + %d weeks + %d أسابيع + + + %@ uploaded + %@ تم الرفع + + + %@, %@ and %lld other members connected + %@, %@ و %lld أعضاء آخرين متصلين + + + %lld seconds + %lld ثواني + + + %u messages failed to decrypt. + %u فشلت عملية فك تشفير الرسائل. + + + %lld messages marked deleted + %lld الرسائل معلمه بالحذف + + + %lld messages moderated by %@ + %lld رسائل تمت إدارتها بواسطة %@ + + + %lld new interface languages + %lld لغات واجهة جديدة + + + %lld group events + %lld أحداث المجموعة + + + %lld messages blocked by admin + %lld رسائل محظورة بواسطه المسؤول + + + %lld messages blocked + %lld رسائل تم حظرها + + + %u messages skipped. + %u تم تخطي الرسائل. + + + **Create 1-time link**: to create and share a new invitation link. + **إضافة جهة اتصال**: لإنشاء رابط دعوة جديد، أو الاتصال عبر الرابط الذي تلقيتوهم. + + + **Create group**: to create a new group. + **إنشاء مجموعة**: لإنشاء مجموعة جديدة. + + + (this device v%@) + (هذا الجهاز v%@) + + + (new) + (جديد) + + + **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **يرجى الملاحظة**: سيؤدي استخدام نفس قاعدة البيانات على جهازين إلى كسر فك تشفير الرسائل من اتصالاتكم كحماية أمنية. + + + A new random profile will be shared. + سيتم مشاركة ملف تعريفي عشوائي جديد. + + + 30 seconds + 30 ثانيه + + + - more stable message delivery. +- a bit better groups. +- and more! + - تسليم رسائل أكثر استقرارًا. +- مجموعات أفضل قليلاً. +- والمزيد! + + + 0 sec + 0 ثانيه + + + 1 minute + 1 دقيقة + + + 5 minutes + 5 دقائق + + + <p>Hi!</p> +<p><a href="%@">Connect to me via SimpleX Chat</a></p> + <p>مرحبا!</p> +<p><a href="%@">أتصل بى من خلال SimpleX Chat</a></p> + + + 0s + 0 ث + + + A few more things + بعض الأشياء الأخرى + + + - 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. + - أتصل بـ [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). +- أسرع و أكثر اسْتِقْرارًا. + + + **Warning**: the archive will be removed. + **تحذير**: سيتم إزالة الأرشيف. + + + - optionally notify deleted contacts. +- profile names with spaces. +- and more! + - إخطار جهات الاتصال المحذوفة بشكل اختياري. +- أسماء الملفات الشخصية مع المسافات. +- والمزيد! + + + - voice messages up to 5 minutes. +- custom time to disappear. +- editing history. + - رسائل صوتية تصل مدتها إلى 5 دقائق. +- وقت مخصص للاختفاء. +- تعديل السجل. + + + Add welcome message + إضافة رسالة ترحيب + + + Abort changing address? + هل تريد إلغاء تغيير العنوان؟ + + + Add contact + إضافة جهة اتصال + + + Abort + إحباط + + + About SimpleX address + حول عنوان SimpleX + + + Accept connection request? + قبول طلب الاتصال؟ + + + Acknowledged + معترف به + + + Acknowledgement errors + أخطاء الإقرار + + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. + أضف عنوانًا إلى ملفكم الشخصي، حتى تتمكن جهات الاتصال الخاصة بكم من مشاركته مع أشخاص اخرين. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بكم. + + + Abort changing address + إحباط تغيير العنوان + + + Active connections + اتصالات نشطة + + + Apply + طبّق + + + %@ server + %@ خادم + + + Accept conditions + اقبل الشروط + + + Share address + مشاركة العنوان + + + Already connecting! + جارٍ الاتصال بالفعل! + + + %d file(s) are still being downloaded. + %d الملف(ات) لا تزال قيد التنزيل. + + + %d file(s) failed to download. + %d الملف(ات) فشلت في التنزيل. + + + All app data is deleted. + حُذفت جميع بيانات التطبيق. + + + Allow irreversible message deletion only if your contact allows it to you. (24 hours) + السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة) + + + Share profile + شارك ملف التعريف + + + Always use relay + استخدم الموجه دائمًا + + + Address + عنوان + + + All data is erased when it is entered. + يتم مسح جميع البيانات عند إدخالها. + + + %d file(s) were deleted. + %d تم حذف الملف(ات). + + + %d file(s) were not downloaded. + %d لم يتم تنزيل الملف(ات). + + + %d messages not forwarded + %d الرسائل لم يتم تحويلها + + + %d seconds(s) + %d ثواني + + + **Scan / Paste link**: to connect via a link you received. + **امسح / ألصِق الرابط**: للاتصال عبر الرابط الذي تلقيته. + + + 1 year + سنة واحدة + + + 1-time link + رابط لمرة واحدة + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + يمكن استعمال الرابط لمرة واحدة *مع جهة اتصال واحدة فقط* - شاركه شخصياً أو عبر أي تطبيق مراسلة. + + + Accent + لون تمييزي + + + Accepted conditions + الشروط المتفق عليها + + + All chats will be removed from the list (text), and the list deleted. + سيتم إزالة جميع الدردشات من القائمة (النص)، وحذف القائمة. + + + Allow message reactions. + السماح بردود الفعل على الرسائل. + + + Allow to irreversibly delete sent messages. (24 hours) + السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) + + + Allow to send SimpleX links. + السماح بإرسال روابط SimpleX. + + + Already joining the group! + جارٍ انضمام بالفعل إلى المجموعة! + + + An empty chat profile with the provided name is created, and the app opens as usual. + يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم، ويفتح التطبيق كالمعتاد. + + + Authentication cancelled + ألغيت المصادقة + + + Audio/video calls are prohibited. + مكالمات الصوت/الفيديو محظورة. + + + Better groups + مجموعات أفضل + + + Background + الخلفية + + + Better calls + مكالمات أفضل + + + Both you and your contact can irreversibly delete sent messages. (24 hours) + يمكنك أنت وجهة اتصالك حذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) + + + Block member for all? + حظر العضو للجميع؟ + + + Blur media + تمويه الوسائط + + + Server type + نوع الخادم + + + Server requires authorization to upload, check password + يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور + + + Server version is incompatible with network settings. + إصدار الخادم غير متوافق مع إعدادات الشبكة. + + + Share with contacts + مشاركة مع جهات الاتصال + + + Show: + عرض: + + + SimpleX Address + عنوان SimpleX + + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + توصلت SimpleX Chat وFlux إلى اتفاق لتضمين الخوادم التي تديرها Flux في التطبيق. + + + Allow calls? + السماح بالمكالمات؟ + + + App passcode is replaced with self-destruct passcode. + يتم استبدال رمز مرور التطبيق برمز مرور التدمير الذاتي. + + + SimpleX Lock mode + SimpleX وضع القفل + + + Audio and video calls + مكالمات الصوت والفيديو + + + App passcode + رمز مرور التطبيق + + + Bad message ID + معرّف رسالة سيئ + + + Server address is incompatible with network settings. + عنوان الخادم غير متوافق مع إعدادات الشبكة. + + + Servers statistics will be reset - this cannot be undone! + سيتم تصفير إحصائيات الخوادم - لا يمكن التراجع عن هذا! + + + Allow to send files and media. + السماح بإرسال الملفات والوسائط. + + + App encrypts new local files (except videos). + يُعمِّي الملفات المحلية الجديدة (باستثناء مقاطع الفيديو). + + + Better messages + رسائل أفضل + + + Set passcode + عيّن رمز المرور + + + Additional accent 2 + لون إضافي ثانوي 2 + + + Allow your contacts adding message reactions. + السماح لجهات اتصالك بإضافة ردود الفعل للرسالة. + + + Allow your contacts to call you. + السماح لجهات اتصالك بالاتصال بك. + + + Audio/video calls + مكالمات الصوت/الفيديو + + + Better notifications + إشعارات أفضل + + + Better user experience + تجربة مستخدم أفضل + + + Block + حظر + + + Black + أسود + + + Block member? + حظر العضو؟ + + + Blocked by admin + محظور من قبل المُدير + + + Blur for better privacy. + تمويه من أجل خصوصية أفضل. + + + Show → on messages sent via private routing. + عرض ← على الرسائل المرسلة عبر التوجيه الخاص. + + + Share from other apps. + المشاركة من التطبيقات الأخرى. + + + Share this 1-time invite link + شارك رابط الدعوة هذا لمرة واحدة + + + Set passphrase + عيّن عبارة المرور + + + Share address with contacts? + مشاركة العنوان مع جهات الاتصال؟ + + + Allow downgrade + السماح بالرجوع إلى إصدار سابق + + + Bad desktop address + عنوان سطح المكتب غير صالح + + + %1$@, %2$@ + %1$@, %2$@ + + + All profiles + جميع ملفات التعريف + + + Authentication is required before the call is connected, but you may miss calls. + يتطلب التوثيق قبل الاتصال بالمكالمة، ولكن قد تفوتك المكالمات. + + + Archiving database + جارِ أرشفة قاعدة البيانات + + + Settings were changed. + تم تغيير الإعدادات. + + + Better groups performance + أداء مجموعات أفضل + + + Better privacy and security + خصوصية وأمان أفضل + + + Better security ✅ + أمان أفضل ✅ + + + Block for all + حظر للجميع + + + Block group members + حظر أعضاء المجموعة + + + Block member + حظر العضو + + + Both you and your contact can add message reactions. + يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل. + + + Both you and your contact can make calls. + يمكنك أنت وجهة الاتصال إجراء مكالمات. + + + Server + الخادم + + + Server operators + مُشغلي الخادم + + + Server version is incompatible with your app: %@. + إصدار الخادم غير متوافق مع التطبيق لديك: %@. + + + Servers info + معلومات الخوادم + + + Set chat name… + عيّن اسم الدردشة… + + + Shape profile images + شكّل الصور التعريفية + + + Share address publicly + شارك العنوان علناً + + + Show developer options + عرض خيارات المطور + + + SimpleX address + عنوان SimpleX + + + SimpleX address or 1-time link? + عنوان SimpleX أو رابط لمرة واحدة؟ + + + @'%@' + @'%@' + + + @%@ + @%@ + + + Active + نشط + + + Add friends + أضف أصدقاء + + + Add list + أضف القائمة + + + Address change will be aborted. Old receiving address will be used. + سيتم إحباط تغيير العنوان. سيتم استخدام عنوان الاستلام القديم. + + + All messages will be deleted - this cannot be undone! + سيتم حذف كافة الرسائل - لا يمكن التراجع عن هذا! + + + All reports will be archived for you. + سيتم أرشفة كافة البلاغات لك. + + + All your contacts will remain connected. + ستبقى جميع جهات اتصالك متصلة. + + + All your contacts will remain connected. Profile update will be sent to your contacts. + ستبقى جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك. + + + All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + جميع جهات الاتصال، المحادثات والملفات الخاصة بك سيتم تشفيرها بأمان ورفعها على شكل أجزاء إلى موجهات XFTP المُعدة. + + + Allow calls only if your contact allows them. + السماح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك. + + + Allow message reactions only if your contact allows them. + اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك. + + + Allow to report messsages to moderators. + السماح بالإبلاغ عن الرسائل إلى المشرفين. + + + Allow your contacts to irreversibly delete sent messages. (24 hours) + اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) + + + Another reason + سبب آخر + + + App group: + مجموعة التطبيق: + + + Apply to + طبّق لِ + + + Archive + أرشف + + + Archive %lld reports? + أرشف تقارير %lld؟ + + + Archive all reports? + أرشفة كافة البلاغات؟ + + + Archive and upload + أرشفة و رفع + + + Archive report + أرشف البلاغ + + + Archive report? + أرشف البلاغ؟ + + + Archive reports + أرشف البلاغات + + + Ask + اسأل + + + Auto-accept settings + إعدادات القبول التلقائي + + + Better message dates. + تواريخ أفضل للرسائل. + + + Server added to operator %@. + تمت إضافة الخادم إلى المشغل %@. + + + Server address is incompatible with network settings: %@. + عنوان الخادم غير متوافق مع إعدادات الشبكة: %@. + + + Server protocol changed. + تغيّر بروتوكول الخادم. + + + SimpleX links are prohibited. + روابط SimpleX محظورة. + + + Additional accent + لون إضافي ثانوي + + + Always use private routing. + استخدم دائمًا التوجيه الخاص. + + + About operators + عن المُشغلين + + + Add team members + أضف أعضاء الفريق + + + Added media & file servers + أُضيفت خوادم الوسائط والملفات + + + Added message servers + أُضيفت خوادم الرسائل + + + Address or 1-time link? + عنوان أو رابط لمرة واحدة؟ + + + Address settings + إعدادات العنوان + + + Allow sharing + السماح بالمشاركة + + + App data migration + ترحيل بيانات التطبيق + + + Archive contacts to chat later. + أرشفة جهات الاتصال للدردشة لاحقًا. + + + Better networking + اتصال أفضل + + + Session code + رمز الجلسة + + + Set default theme + تعيين السمة الافتراضية + + + Set it instead of system authentication. + عيّنها بدلاً من استيثاق النظام. + + + Set the message shown to new members! + تعيين رسالة تظهر للأعضاء الجدد! + + + Share 1-time link + مشاركة رابط ذو استخدام واحد + + + Share 1-time link with a friend + شارك رابطًا لمرة واحدة مع صديق + + + Share SimpleX address on social media. + شارك عنوان SimpleX على وسائل التواصل الاجتماعي. + + + Share to SimpleX + المشاركة لSimpleX + + + Show calls in phone history + عرض المكالمات في سجل الهاتف + + + Show percentage + أظهِر النسبة المئوية + + + SimpleX + SimpleX + + + SimpleX Lock not enabled! + قفل SimpleX غير مفعّل! + + + Bad message hash + تجزئة رسالة سيئة + + + App session + جلسة التطبيق + + + SimpleX links not allowed + روابط SimpleX غير مسموح بها + + + All data is kept private on your device. + جميع البيانات تُحفظ بشكل خاص على جهازك. + + + Archived contacts + جهات الاتصال المؤرشفة + + + Show message status + أظهِر حالة الرسالة + + + Set message expiration in chats. + اضبط انتهاء صلاحية الرسالة في الدردشات. + + + Server address + عنوان الخادم + + + Show last messages + إظهار الرسائل الأخيرة + + + Server operator changed. + تغيّر مُشغل الخادم. + + + SimpleX address and 1-time links are safe to share via any messenger. + عنوان SimpleX والروابط لمرة واحدة آمنة للمشاركة عبر أي برنامج مُراسلة. + + + Add your team members to the conversations. + أضف أعضاء فريقك إلى المحادثات. + + + Advanced settings + إعدادات متقدّمة + + + Add to list + أضف إلى القائمة + + + Additional secondary + ثانوي إضافي + + + Admins can block a member for all. + يمكن للمُدراء حظر عضو للجميع. + + + All + الكل + + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + جميع الرسائل والملفات تُرسل **مشفرة من النهاية-إلى-النهاية**، مع أمان ما-بعد-الحوسبة-الكمية في الرسائل المباشرة. + + + All new messages from %@ will be hidden! + جميع الرسائل الجديدة من %@ سيتم إخفاؤها! + + + Auto-accept + قبول تلقائي + + + Change self-destruct mode + تغيير وضع التدمير الذاتي + + + Chat database exported + صُدرت قاعدة بيانات الدردشة + + + Businesses + الشركات + + + Change automatic message deletion? + تغيير حذف الرسائل التلقائي؟ + + + Can't call contact + لا يمكن مكالمة جهة الاتصال + + + Chat list + قائمة الدردشات + + + Calls prohibited! + المكالمات ممنوعة! + + + Change lock mode + تغيير وضع القفل + + + Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. + توقفت الدردشة. إذا كنت قد استخدمت قاعدة البيانات هذه بالفعل على جهاز آخر، فيجب عليك نقلها مرة أخرى قبل بدء الدردشة. + + + Cellular + خلوي + + + Chat + الدردشة + + + Chat already exists! + الدردشة موجودة بالفعل! + + + Chat will be deleted for you - this cannot be undone! + سيتم حذف الدردشة لديك - لا يمكن التراجع عن هذا! + + + Chat will be deleted for all members - this cannot be undone! + سيتم حذف الدردشة لجميع الأعضاء - لا يمكن التراجع عن هذا! + + + Change self-destruct passcode + تغيير رمز المرور التدمير الذاتي + + + Camera not available + الكاميرا غير متوفرة + + + Capacity exceeded - recipient did not receive previously sent messages. + تم تجاوز السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا. + + + Change passcode + تغيير رمز المرور + + + Chat colors + ألوان الدردشة + + + Chat theme + سمة الدردشة + + + Business address + عنوان العمل التجاري + + + Business chats + دردشات العمل التجاري + + + Cancel migration + ألغِ الترحيل + + + Change chat profiles + غيّر ملفات تعريف الدردشة + + + Chat migrated! + رحّلت الدردشة! + + + Chat profile + ملف تعريف الدردشة + + + Contact deleted! + حُذفت جهة الاتصال! + + + Conditions of use + شروط الاستخدام + + + Connecting + جارِ الاتصال + + + Connect incognito + اتصال متخفي + + + Created at + أُنشئ في + + + Connect via contact address + الاتصال عبر عنوان جهة الاتصال + + + Connected servers + الخوادم المتصلة + + + standard end-to-end encryption + التعمية القياسية بين الطرفين + + + Delete up to 20 messages at once. + حذف ما يصل إلى 20 رسالة في آن واحد. + + + Connect to your friends faster. + تواصل مع أصدقائك بشكل أسرع. + + + Developer options + خيارات المطور + + + Connect to yourself? + اتصل بنفسك؟ + + + Connect via one-time link + اتصال عبر رابط لمرة واحدة + + + Connect to yourself? +This is your own SimpleX address! + اتصل بنفسك؟ +هذا هو عنوان SimpleX الخاص بك! + + + Connecting to contact, please wait or check later! + جارِ الاتصال بجهة الاتصال، يُرجى الانتظار أو التحقق لاحقًا! + + + Database upgrade + ترقية قاعدة البيانات + + + Create list + أنشئ قائمة + + + Create profile + إنشاء ملف تعريف + + + Creating archive link + جارِ إنشاء رابط الأرشيف + + + Details + التفاصيل + + + Customize theme + تخصيص السمة + + + Dark mode colors + ألوان الوضع الداكن + + + Delete and notify contact + حذف وإشعار جهة الاتصال + + + Deleted at: %@ + حُذفت في: %@ + + + Detailed statistics + إحصائيات مفصلة + + + you are observer + أنت المراقب + + + you + أنت + + + when IP hidden + عندما يكون IP مخفيًا + + + video + فيديو + + + Clear or delete group? + مسح أو حذف المجموعة؟ + + + Clear private notes? + مسح الملاحظات الخاصة؟ + + + Community guidelines violation + انتهاك إرشادات المجتمع + + + Connection not ready. + الاتصال غير جاهز. + + + Connection requires encryption renegotiation. + يتطلب الاتصال إعادة التفاوض على التعمية. + + + Contact is deleted. + حُذفت جهة الاتصال. + + + Contacts + جهات الاتصال + + + Create SimpleX address + أنشئ عنوان SimpleX + + + Current conditions text couldn't be loaded, you can review conditions via this link: + لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط: + + + Delete chat messages from your device. + احذف رسائل الدردشة من جهازك. + + + Delete or moderate up to 200 messages. + حذف أو إشراف ما يصل إلى 200 رسالة. + + + Delete profile + حذف ملف التعريف + + + Desktop devices + أجهزة سطح المكتب + + + set new profile picture + عيّن صورة تعريفية جديدة + + + weeks + أسابيع + + + Chunks uploaded + رُفع القطع + + + Color mode + وضع اللون + + + Created + أُنشئت + + + Current Passcode + رمز المرور الحالي + + + Custom time + وقت مخصّص + + + Debug delivery + تسليم التصحيح + + + Deleted + حُذفت + + + Delete file + حذف الملف + + + unknown status + حالة غير معروفة + + + unknown servers + خوادم غير معروفة + + + Connect to yourself? +This is your own one-time link! + اتصل بنفسك؟ +هذا هو الرابط الخاص بك لمرة واحدة! + + + Connect with %@ + الاتصال ب%@ + + + Connected desktop + سطح المكتب متصل + + + Connected to desktop + متصل بسطح المكتب + + + Conversation deleted! + حُذفت المحادثة! + + + Create a group using a random profile. + أنشئ مجموعة باستخدام ملف تعريف عشوائي. + + + Delete chat + احذف الدردشة + + + Delete chat profile + حذف ملف تعريف الدردشة + + + Delete chat? + حذف الدردشة؟ + + + Delete database from this device + احذف قاعدة البيانات من هذا الجهاز + + + Delivery + التوصيل + + + Delivery receipts are disabled! + إيصالات التسليم مُعطَّلة! + + + Connection terminated + انتهى الاتصال + + + Create file + إنشاء ملف + + + Create group + أنشئ مجموعة + + + Database IDs and Transport isolation option. + معرفات قاعدة البيانات وخيار عزل النقل. + + + Database downgrade + الرجوع إلى إصدار سابق من قاعدة البيانات + + + Delivery receipts! + إيصالات التسليم! + + + Desktop address + عنوان سطح المكتب + + + updated profile + حدّثت ملف التعريف + + + Connect to desktop + اتصل بسطح المكتب + + + Connecting to desktop + جار الاتصال بسطح المكتب + + + Completed + اكتملت + + + Connection notifications + إشعارات الاتصال + + + Connection and servers status. + حالة الاتصال والخوادم. + + + Continue + متابعة + + + Connections + الاتصالات + + + Content violates conditions of use + المحتوى ينتهك شروط الاستخدام + + + Corner + ركن + + + Creating link… + جارِ إنشاء الرابط… + + + Database ID: %d + معرّف قاعدة البيانات: %d + + + Decryption error + خطأ في فك التعمية + + + Delete report + احذف البلاغ + + + Delete without notification + احذف دون إشعار + + + Deleted at + حُذفت في + + + Clear group? + مسح المجموعة؟ + + + Compare file + قارن الملف + + + Connect automatically + اتصل تلقائيًا + + + Connection blocked + حُظر الاتصال + + + unprotected + غير محمي + + + Deletion errors + أخطاء الحذف + + + Conditions will be accepted for enabled operators after 30 days. + سيتم قبول الشروط للمُشغلين المفعّلين بعد 30 يومًا. + + + Connection security + أمان الاتصال + + + Contact will be deleted - this cannot be undone! + سيتم حذف جهة الاتصال - لا يمكن التراجع عن هذا! + + + Copy error + خطأ في النسخ + + + Create 1-time link + أنشئ رابط لمرة واحدة + + + Connected + متصل + + + Current profile + ملف التعريف الحالي + + + Customizable message shape. + شكل الرسالة قابل للتخصيص. + + + Chunks deleted + حُذفت القطع + + + Chinese and Spanish interface + الواجهة الصينية والاسبانية + + + Download + نزّل + + + Downloaded + نُزّلت + + + Downloaded files + الملفات التي نُزّلت + + + Don't show again + لا تُظهر مرة أخرى + + + Confirm contact deletion? + تأكيد حذف جهة الاتصال؟ + + + Confirm database upgrades + تأكيد ترقيات قاعدة البيانات + + + Download failed + فشل التنزيل + + + Download file + نزّل الملف + + + Downloading link details + جارِ تنزيل تفاصيل الرابط + + + Downloading archive + جارِ تنزيل الأرشيف + + + Don't enable + لا تُفعل + + + Confirm upload + أكّد الرفع + + + Chunks downloaded + نُزّلت القطع + + + Confirm Passcode + تأكيد رمز المرور + + + Confirm files from unknown servers. + تأكيد الملفات من خوادم غير معروفة. + + + Confirm network settings + أكّد إعدادات الشبكة + + + Confirm that you remember database passphrase to migrate it. + تأكد من أنك تتذكر عبارة مرور قاعدة البيانات لترحيلها. + + + Downgrade and open chat + الرجوع إلى إصدار سابق وفتح الدردشة + + + Don't miss important messages. + لا تفوت رسائل مهمة. + + + E2E encrypted notifications. + إشعارات مُشفرة بين الطرفين E2E + + + Download errors + أخطاء التنزيل + + + Download files + نزّل الملفات + + + Confirm password + تأكيد كلمة المرور + + + Enable self-destruct + تفعيل التدمير الذاتي + + + Enable (keep overrides) + تفعيل (الاحتفاظ بالتجاوزات) + + + Enable Flux + فعّل flux + + + Enable in direct chats (BETA)! + فعّل في الدردشات المباشرة (تجريبي)! + + + Enable for all + تفعيل للجميع + + + Enable lock + تفعيل القفل + + + Enable camera access + فعّل الوصول إلى الكاميرا + + + Enable self-destruct passcode + تفعيل رمز التدمير الذاتي + + + Can't message member + لا يمكن الاتصال بالعضو + + + Color chats with the new themes. + محادثات ملونة مع السمات الجديدة. + + + All chats will be removed from the list %@, and the list deleted. + ستتم إزالة جميع الدردشات من القائمة %@، وسيتم حذف القائمة. + + + 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)! + + + Choose _Migrate from another device_ on the new device and scan QR code. + اختر _الترحيل من جهاز آخر_ على الجهاز الجديد وامسح رمز الاستجابة السريعة. + + + Conditions will be accepted for the operator(s): **%@**. + سيتم قبول شروط المشغل (المشغلين): **%@**. + + + Conditions will be accepted on: %@. + سيتم قبول الشروط على: %@. + + + Confirmed + تم التأكيد + + + Connection is blocked by server operator: +%@ + تم حظر الاتصال من قبل مشغل الخادم: +%@ + + + Can't call member + لا يمكن الاتصال بالعضو + + + Chat already exists + الدردشة موجودة بالفعل + + + Check messages every 20 min. + تحقق من الرسائل كل 20 دقيقة. + + + Check messages when allowed. + تحقق من الرسائل عندما يُسمح بذلك. + + + Cannot forward message + لا يمكن إعادة توجيه الرسالة + + + Chat preferences were changed. + تم تغيير تفضيلات المحادثة. + + + Conditions are already accepted for these operator(s): **%@**. + الشروط مقبولة بالفعل لهذا المشغل (المشغلين): **%@**. + + + Conditions will be accepted for operator(s): **%@**. + سيتم قبول شروط المشغل (المشغلين): **%@**. + + + Conditions accepted on: %@. + الشروط المقبولة على: %@. + + + Conditions are accepted for the operator(s): **%@**. + يتم قبول شروط المشغل (المشغلين): **%@**. + + + Conditions will be automatically accepted for enabled operators on: %@. + سيتم قبول الشروط تلقائيًا للمشغلين الممكّنين على: %@. + + + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 + أنشئ ملفًا شخصيًا جديدًا في [تطبيق سطح المكتب](https://simplex.chat/downloads/). 💻 + + + Error adding server + خطأ في إضافة الخادم + + + Created at: %@ + تم الإنشاء في: %@ + + + Delete %lld messages of members? + حذف %lld الرسائل القديمة للأعضاء؟ + + + Disappearing message + رسالة اختفاء + + + Enabled + ممكّنة + + + Encrypted message: database migration error + رسالة مشفرة: خطأ في ترحيل قاعدة البيانات + + + Delete list? + Delete list? + + + Delivered even when Apple drops them. + يتم تسليمها حتى عندما تسقطها شركة Apple. + + + Destination server address of %@ is incompatible with forwarding server %@ settings. + عنوان خادم الوجهة %@ غير متوافق مع إعدادات خادم التوجيه %@. + + + Destination server version of %@ is incompatible with forwarding server %@. + إصدار خادم الوجهة لـ %@ غير متوافق مع خادم التوجيه %@. + + + Don't create address + لا تنشئ عنوان + + + Done + تم + + + Duration + المدة + + + Encrypt local files + تشفير الملفات المحلية + + + Encryption renegotiation in progress. + إعادة التفاوض على التشفير قيد التنفيذ. + + + Enter Passcode + أدخل رمز المرور + + + Enter passphrase + قم بأدخل عبارة المرور + + + Enter welcome message… + أدخل رسالة ترحيب… + + + Enter your name… + أدخل اسمك… + + + Error changing to incognito! + خطأ في التغيير إلى التصفح المتخفي! + + + Delete %lld messages? + حذف %lld رسائل؟ + + + Error aborting address change + خطأ في إجهاض تغيير العنوان + + + Disappears at + يختفي عند + + + Do not use credentials with proxy. + لا تستخدم بيانات الاعتماد مع البروكسي. + + + Error accepting conditions + خطأ في قبول الشروط + + + Enter password above to show! + أدخل كلمة المرور أعلاه للعرض! + + + Error changing connection profile + خطأ في تغيير ملف تعريف الاتصال + + + Desktop app version %@ is not compatible with this app. + إصدار تطبيق سطح المكتب %@ غير متوافق مع هذا التطبيق. + + + Encrypt stored files & media + تشفير الملفات والوسائط المخزنة + + + Enter this device name… + أدخل اسم الجهاز… + + + Enter welcome message… (optional) + أدخل رسالة ترحيب... (اختياري) + + + Correct name to %@? + الاسم الصحيح ل %@؟ + + + Delete member message? + حذف رسالة العضو؟ + + + Disable automatic message deletion? + تعطيل حذف الرسائل التلقائي؟ + + + Disable delete messages + تعطيل حذف الرسائل + + + Disable for all + تعطيل للجميع + + + Disabled + عاجز + + + Documents: + المستندات: + + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + باستخدامك SimpleX Chat، فإنك توافق على: +- إرسال محتوى قانوني فقط في المجموعات العامة. +- احترام المستخدمين الآخرين - ممنوع إرسال رسائل مزعجة. + + + Configure server operators + تكوين مشغلي الخادم + + + Enable Flux in Network & servers settings for better metadata privacy. + تمكين التدفق في إعدادات الشبكة والخوادم لتحسين خصوصية البيانات الوصفية. + + + Discover and join groups + اكتشف المجموعات وانضم إليها + + + Discover via local network + اكتشف عبر الشبكة المحلية + + + Enabled for + ممكّن ل + + + Encrypted message: app is stopped + رسالة مشفرة: تم إيقاف التطبيق + + + Enter group name… + أدخل اسم المجموعة… + + + Do NOT use private routing. + لا تستخدم التوجيه الخاص. + + + Encryption re-negotiation error + خطأ في إعادة تفاوض التشفير + + + Connection with desktop stopped + تم إيقاف الاتصال بسطح المكتب + + + Destination server error: %@ + خطأ خادم الوجهة: %@ + + + Do NOT send messages directly, even if your or destination server does not support private routing. + لا ترسل الرسائل بشكل مباشر، حتى لو كان خادمك أو خادم الوجهة لا يدعم التوجيه الخاص. + + + Direct messages between members are prohibited in this chat. + يُحظر إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة. + + + Disconnect desktop? + فصل سطح المكتب؟ + + + Disable (keep overrides) + تعطيل (الاحتفاظ بالتجاوزات) + + + Disappears at: %@ + يختفي عند: %@ + + + Do not send history to new members. + لا ترسل التاريخ إلى الأعضاء الجدد. + + + Encryption re-negotiation failed. + فشل إعادة التفاوض على التشفير. + @@ -3723,4 +5805,80 @@ SimpleX servers cannot see your profile. + + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + يمكنك السماح بالمشاركة في إعدادات الخصوصية والأمان / اعدادات "SimpleX Lock" + + + Keychain error + خطأ في Keychain + + + Invalid migration confirmation + تأكيد الترحيل غير صالح + + + %@ + %@ + + + Share + مشاركة + + + Incompatible database version + إصدار قاعدة بيانات غير متوافق + + + File error + خطأ في الملف + + + Database downgrade required + مطلوب الرجوع إلى إصدار سابق من قاعدة البيانات‎ + + + Database encrypted! + قاعدة البيانات مُعمّاة! + + + Wrong database passphrase + عبارة مرور قاعدة بيانات خاطئة + + + Selected chat preferences prohibit this message. + تفضيلات الدردشة المحدّدة تحظر هذه الرسالة. + + + Database error + خطأ في قاعدة البيانات + + + Database passphrase is required to open chat. + عبارة مرور قاعدة البيانات مطلوبة لفتح الدردشة. + + + Error: %@ + خطأ: %@ + + + Cancel + إلغاء + + + Large file! + الملف كبير! + + + + + + + From: %@ + من: %@ + + + 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 3d0bb2bf2c..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 @@
- +
- - - - - - 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) (може да се копира) @@ -127,9 +100,12 @@ %@ е потвърдено No comment provided by engineer. + + %@ server + No comment provided by engineer. + %@ servers - %@ сървъри No comment provided by engineer. @@ -142,6 +118,11 @@ %@ иска да се свърже! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ и %lld членове @@ -162,11 +143,31 @@ %d дни time interval + + %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. + forward confirmation reason + %d hours %d часа time interval + + %d messages not forwarded + alert title + %d min %d мин. @@ -182,6 +183,10 @@ %d сек. time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d пропуснато(и) съобщение(я) @@ -252,11 +257,6 @@ %lld нови езици на интерфейса No comment provided by engineer. - - %lld second(s) - %lld секунда(и) - No comment provided by engineer. - %lld seconds %lld секунди @@ -307,11 +307,6 @@ %u пропуснати съобщения. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (ново) @@ -322,19 +317,9 @@ (това устройство v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Добави контакт**: за създаване на нов линк или свързване чрез получен линк за връзка. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт. + + **Create 1-time link**: to create and share a new invitation link. + **Добави контакт**: за създаване на нов линк. No comment provided by engineer. @@ -342,13 +327,13 @@ **Създай група**: за създаване на нова група. 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. + + **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). + + **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. @@ -362,11 +347,15 @@ **Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите. 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. + + **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 / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Внимание**: Незабавните push известия изискват парола, запазена в Keychain. @@ -392,11 +381,6 @@ \*удебелен* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +417,6 @@ - история на редактиране. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -451,7 +430,8 @@ 1 day 1 ден - time interval + delete after time +time interval 1 hour @@ -466,12 +446,27 @@ 1 month 1 месец - time interval + delete after time +time interval 1 week 1 седмица - time interval + delete after time +time interval + + + 1 year + delete after time + + + 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 @@ -488,11 +483,6 @@ 30 секунди No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -542,31 +532,32 @@ Откажи смяна на адрес? No comment provided by engineer. - - About SimpleX - За SimpleX - No comment provided by engineer. - About SimpleX Chat За SimpleX Chat No comment provided by engineer. - - About SimpleX address - Повече за SimpleX адреса + + About operators + За операторите No comment provided by engineer. - - Accent color - Основен цвят + + Accent + Акцент No comment provided by engineer. Accept Приеми accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Приеми условията + No comment provided by engineer. Accept connection request? @@ -581,21 +572,45 @@ Accept incognito Приеми инкогнито - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Приети условия + No comment provided by engineer. + + + Acknowledged + Потвърден + No comment provided by engineer. + + + Acknowledgement errors + Грешки при потвърждението + No comment provided by engineer. + + + Active + token status text + + + 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 contact - Добави контакт + + Add friends + Добави приятели No comment provided by engineer. - - Add preset servers - Добави предварително зададени сървъри + + Add list No comment provided by engineer. @@ -603,14 +618,19 @@ Добави профил No comment provided by engineer. + + Add server + Добави сървър + No comment provided by engineer. + Add servers by scanning QR codes. Добави сървъри чрез сканиране на QR кодове. No comment provided by engineer. - - Add server… - Добави сървър… + + Add team members + Добави членове на екипа No comment provided by engineer. @@ -618,11 +638,45 @@ Добави към друго устройство 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 Адрес @@ -633,6 +687,16 @@ Промяната на адреса ще бъде прекъсната. Ще се използва старият адрес за получаване. No comment provided by engineer. + + Address or 1-time link? + Адрес или еднократен линк? + No comment provided by engineer. + + + Address settings + Настройки на адреса + No comment provided by engineer. + Admins can block a member for all. Администраторите могат да блокират член за всички. @@ -648,6 +712,15 @@ Разширени мрежови настройки No comment provided by engineer. + + Advanced settings + Разширени настройки + No comment provided by engineer. + + + All + No comment provided by engineer. + All app data is deleted. Всички данни от приложението бяха изтрити. @@ -658,16 +731,29 @@ Всички чатове и съобщения ще бъдат изтрити - това не може да бъде отменено! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + alert message + 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. Всички членове на групата ще останат свързани. 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! Всички съобщения ще бъдат изтрити - това не може да бъде отменено! @@ -683,6 +769,19 @@ Всички нови съобщения от %@ ще бъдат скрити! No comment provided by engineer. + + All profiles + Всички профили + profile dropdown + + + 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. Всички ваши контакти ще останат свързани. @@ -708,11 +807,21 @@ Позволи обаждания само ако вашият контакт ги разрешава. 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 downgrade + Позволи понижаване + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа) @@ -738,11 +847,20 @@ Разреши изпращането на изчезващи съобщения. No comment provided by engineer. + + 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 SimpleX links. Разрешаване на изпращане на SimpleX линкове. @@ -808,6 +926,11 @@ Вече се присъединихте към групата! No comment provided by engineer. + + Always use private routing. + Винаги използвай поверително рутиране. + No comment provided by engineer. + Always use relay Винаги използвай реле @@ -818,11 +941,20 @@ Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено. No comment provided by engineer. + + Another reason + report reason + Answer call Отговор на повикване No comment provided by engineer. + + Anybody can host servers. + Протокол и код с отворен код – всеки може да оперира собствени сървъри. + No comment provided by engineer. + App build: %@ Компилация на приложението: %@ @@ -838,6 +970,10 @@ Приложението криптира нови локални файлове (с изключение на видеоклипове). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon Икона на приложението @@ -853,6 +989,11 @@ Кода за достъп до приложение се заменя с код за самоунищожение. No comment provided by engineer. + + App session + Сесия на приложението + No comment provided by engineer. + App version Версия на приложението @@ -873,11 +1014,50 @@ Приложи No comment provided by engineer. + + Apply to + Приложи към + No comment provided by engineer. + + + Archive + No comment provided by engineer. + + + Archive %lld reports? + 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? + No comment provided by engineer. + + + Archive reports + swipe action + + + Archived contacts + Архивирани контакти + No comment provided by engineer. + Archiving database Архивиране на база данни @@ -943,11 +1123,21 @@ Автоматично приемане на изображения No comment provided by engineer. + + Auto-accept settings + Автоматично приемане на настройки + alert title + Back Назад No comment provided by engineer. + + Background + Фон + No comment provided by engineer. + Bad desktop address Грешен адрес на настолното устройство @@ -963,16 +1153,59 @@ Лош хеш на съобщението 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 Блокирай @@ -1008,6 +1241,16 @@ Блокиран от админ No comment provided by engineer. + + Blur for better privacy. + Размазване за по-добра поверителност. + No comment provided by engineer. + + + Blur media + Размазване на медия + No comment provided by engineer. + Both you and your contact can add message reactions. И вие, и вашият контакт можете да добавяте реакции към съобщението. @@ -1038,11 +1281,31 @@ Български, финландски, тайландски и украински - благодарение на потребителите и [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: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! Разговорът вече приключи! @@ -1053,11 +1316,26 @@ Обаждания No comment provided by engineer. + + 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! Не може да покани контакта! @@ -1068,10 +1346,15 @@ Не може да поканят контактите! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel Отказ - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1366,19 @@ Няма достъп до Keychain за запазване на паролата за базата данни No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Файлът не може да бъде получен - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Капацитетът е надвишен - получателят не е получил предишно изпратените съобщения. + snd error text Cellular @@ -1098,6 +1390,15 @@ Промени No comment provided by engineer. + + Change automatic message deletion? + alert title + + + Change chat profiles + Промени чат профилите + authentication reason + Change database passphrase? Промяна на паролата на базата данни? @@ -1142,11 +1443,22 @@ Change self-destruct passcode Промени кода за достъп за самоунищожение authentication reason - set passcode view +set passcode view - - 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. @@ -1156,7 +1468,7 @@ Chat database - База данни за чата + База данни No comment provided by engineer. @@ -1164,9 +1476,13 @@ Базата данни на чата е изтрита No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported - Базата данни на чат е импортирана + Базата данни на е импортирана No comment provided by engineer. @@ -1184,6 +1500,10 @@ Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново. No comment provided by engineer. + + Chat list + No comment provided by engineer. + Chat migrated! Чатът е мигриран! @@ -1194,15 +1514,44 @@ Чат настройки No comment provided by engineer. + + Chat preferences were changed. + alert message + + + 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. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Проверете адреса на сървъра и опитайте отново. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1573,22 @@ Избери от библиотеката No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear Изчисти - No comment provided by engineer. + swipe action Clear conversation @@ -1239,6 +1600,14 @@ Изчисти разговора? No comment provided by engineer. + + Clear group? + No comment provided by engineer. + + + Clear or delete group? + No comment provided by engineer. + Clear private notes? Изчистване на лични бележки? @@ -1249,11 +1618,18 @@ Изчисти проверката No comment provided by engineer. - - Colors - Цветове + + Color chats with the new themes. No comment provided by engineer. + + Color mode + No comment provided by engineer. + + + Community guidelines violation + report reason + Compare file Сравни файл @@ -1264,11 +1640,47 @@ Сравнете кодовете за сигурност с вашите контакти. 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 Потвърди @@ -1279,11 +1691,19 @@ Потвърди kодa за достъп 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 Потвърди мрежовите настройки @@ -1309,6 +1729,10 @@ Потвърди качването No comment provided by engineer. + + Confirmed + token status text + Connect Свързване @@ -1329,6 +1753,10 @@ Свързване с настолно устройство No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? Свърване със себе си? @@ -1368,16 +1796,28 @@ This is your own one-time link! Свързване с %@ 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 to server… Свързване със сървъра… @@ -1388,6 +1828,10 @@ This is your own one-time link! Свързване със сървър…(грешка: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop Свързване с настолно устройство @@ -1398,6 +1842,14 @@ This is your own one-time link! Връзка No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + + + Connection blocked + No comment provided by engineer. + Connection error Грешка при свързване @@ -1408,11 +1860,32 @@ This is your own one-time link! Грешка при свързване (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + 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 Връзката е прекратена @@ -1423,6 +1896,14 @@ This is your own one-time link! Времето на изчакване за установяване на връзката изтече No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows Контактът позволява @@ -1433,6 +1914,10 @@ This is your own one-time link! Контактът вече съществува No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: Контактът е скрит: @@ -1443,9 +1928,8 @@ This is your own one-time link! Контактът е свързан notification - - Contact is not connected yet! - Контактът все още не е свързан! + + Contact is deleted. No comment provided by engineer. @@ -1458,6 +1942,10 @@ This is your own one-time link! Настройки за контакт No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Контакти @@ -1468,21 +1956,37 @@ This is your own one-time link! Контактите могат да маркират съобщения за изтриване; ще можете да ги разглеждате. No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue Продължи No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy Копирай - chat item action + 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 %@? Поправи име на %@? @@ -1490,32 +1994,32 @@ This is your own one-time link! Create - Създай + Създаване + No comment provided by engineer. + + + Create 1-time link + Създаване на еднократна препратка No comment provided by engineer. Create SimpleX address - Създай SimpleX адрес + Създаване на адрес в SimpleX No comment provided by engineer. Create a group using a random profile. - Създай група с автоматично генериран профилл. - No comment provided by engineer. - - - Create an address to let people connect with you. - Създайте адрес, за да позволите на хората да се свързват с вас. + Създаване група с автоматично създаден профил. No comment provided by engineer. Create file - Създай файл + Създаване на файл server test step Create group - Създай група + Създаване на група No comment provided by engineer. @@ -1528,6 +2032,10 @@ This is your own one-time 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/). 💻 @@ -1553,6 +2061,10 @@ This is your own one-time link! Създай своя профил No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Създаден на @@ -1563,11 +2075,6 @@ This is your own one-time link! Създаден на: %@ copied message info - - Created on %@ - Създаден на %@ - No comment provided by engineer. - Creating archive link Създаване на архивен линк @@ -1583,11 +2090,19 @@ This is your own one-time link! Текущ kод за достъп 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 passphrase… Текуща парола… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. В момента максималният поддържан размер на файла е %@. @@ -1598,11 +2113,23 @@ This is your own one-time link! Персонализирано време 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 ID ID в базата данни @@ -1701,6 +2228,10 @@ This is your own one-time link! Базата данни ще бъде мигрирана, когато приложението се рестартира No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized Децентрализиран @@ -1714,18 +2245,18 @@ This is your own one-time link! Delete Изтрий - chat item action + alert action +swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? Изтриване на %lld съобщения? No comment provided by engineer. - - Delete Contact - Изтрий контакт - No comment provided by engineer. - Delete address Изтрий адрес @@ -1751,14 +2282,12 @@ This is your own one-time link! Изтрий и уведоми контакт 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. @@ -1771,6 +2300,10 @@ This is your own one-time link! Изтриване на чат профила? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Изтрий връзката @@ -1781,11 +2314,8 @@ This is your own one-time link! Изтрий контакт No comment provided by engineer. - - Delete contact? -This cannot be undone! - Изтрий контакт? -Това не може да бъде отменено! + + Delete contact? No comment provided by engineer. @@ -1848,6 +2378,10 @@ This cannot be undone! Изтрий линк? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Изтрий съобщението на члена? @@ -1861,7 +2395,7 @@ This cannot be undone! Delete messages Изтрий съобщенията - No comment provided by engineer. + alert button Delete messages after @@ -1878,9 +2412,8 @@ This cannot be undone! Изтрий старата база данни? No comment provided by engineer. - - Delete pending connection - Изтрий предстоящата връзка + + Delete or moderate up to 200 messages. No comment provided by engineer. @@ -1898,11 +2431,27 @@ This cannot be undone! Изтрий опашка server test step + + Delete report + No comment provided by engineer. + + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Изтрий потребителския профил? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at Изтрито на @@ -1913,6 +2462,14 @@ This cannot be undone! Изтрито на: %@ copied message info + + Deletion errors + No comment provided by engineer. + + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Доставка @@ -1948,11 +2505,35 @@ This cannot be undone! Настолни устройства No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + 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 Инструменти за разработчици @@ -1983,8 +2564,12 @@ This cannot be undone! Лични съобщения chat feature - - 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. @@ -1998,11 +2583,23 @@ This cannot be undone! Деактивирай SimpleX заключване authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Деактивиране за всички No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Изчезващо съобщение @@ -2018,8 +2615,8 @@ This cannot be undone! Изчезващите съобщения са забранени в този чат. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Изчезващите съобщения са забранени в тази група. No comment provided by engineer. @@ -2053,11 +2650,19 @@ This cannot be undone! Откриване през локалната мрежа 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 SimpleX for emergency calls. НЕ използвайте SimpleX за спешни повиквания. No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later Отложи @@ -2068,6 +2673,14 @@ This cannot be undone! Не изпращай история на нови членове. No comment provided by engineer. + + Do not use credentials with proxy. + No comment provided by engineer. + + + Documents: + No comment provided by engineer. + Don't create address Не създавай адрес @@ -2078,11 +2691,19 @@ This cannot be undone! Не активирай 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 Понижи версията и отвори чата @@ -2091,7 +2712,12 @@ This cannot be undone! Download Изтегли - chat item action + alert button +chat item action + + + Download errors + No comment provided by engineer. Download failed @@ -2103,6 +2729,18 @@ This cannot be undone! Свали файл server test step + + Download files + alert action + + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Архива се изтегля @@ -2123,6 +2761,10 @@ This cannot be undone! Продължителност No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Редактирай @@ -2143,6 +2785,10 @@ This cannot be undone! Активиране (запазване на промените) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock Активирай SimpleX заключване @@ -2156,7 +2802,7 @@ This cannot be undone! Enable automatic message deletion? Активиране на автоматично изтриване на съобщения? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2849,10 @@ This cannot be undone! Активирай kод за достъп за самоунищожение set passcode view + + Enabled + No comment provided by engineer. + Enabled for Активирано за @@ -2278,6 +2928,10 @@ This cannot be undone! Неуспешно повторно договаряне на криптирането. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Въведете kодa за достъп @@ -2343,26 +2997,33 @@ This cannot be undone! Грешка при отказване на промяна на адреса No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Грешка при приемане на заявка за контакт No comment provided by engineer. - - Error accessing database file - Грешка при достъпа до файла с базата данни - No comment provided by engineer. - Error adding member(s) Грешка при добавяне на член(ове) No comment provided by engineer. + + Error adding server + alert title + Error changing address Грешка при промяна на адреса No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Грешка при промяна на ролята @@ -2373,6 +3034,18 @@ This cannot be undone! Грешка при промяна на настройката 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 Грешка при създаване на адрес @@ -2388,6 +3061,10 @@ This cannot be undone! Грешка при създаване на групов линк No comment provided by engineer. + + Error creating list + alert title + Error creating member contact Грешка при създаване на контакт с член @@ -2403,6 +3080,10 @@ This cannot be undone! Грешка при създаване на профил! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Грешка при декриптирането на файла @@ -2410,7 +3091,7 @@ This cannot be undone! Error deleting chat database - Грешка при изтриване на чат базата данни + Грешка при изтриване на базата данни No comment provided by engineer. @@ -2423,11 +3104,6 @@ This cannot be undone! Грешка при изтриване на връзката No comment provided by engineer. - - Error deleting contact - Грешка при изтриване на контакт - No comment provided by engineer. - Error deleting database Грешка при изтриване на базата данни @@ -2470,12 +3146,16 @@ This cannot be undone! 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. @@ -2483,9 +3163,12 @@ This cannot be undone! Грешка при присъединяване към група No comment provided by engineer. - - Error loading %@ servers - Грешка при зареждане на %@ сървъри + + Error loading servers + alert title + + + Error migrating settings No comment provided by engineer. @@ -2496,16 +3179,31 @@ This cannot be undone! Error receiving file Грешка при получаване на файл + alert title + + + Error reconnecting server No comment provided by engineer. + + Error reconnecting servers + No comment provided by engineer. + + + Error registering for notifications + alert title + Error removing member Грешка при отстраняване на член No comment provided by engineer. - - Error saving %@ servers - Грешка при запазване на %@ сървъра + + Error reordering lists + alert title + + + Error resetting statistics No comment provided by engineer. @@ -2513,6 +3211,10 @@ This cannot be undone! Грешка при запазване на ICE сървърите No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile Грешка при запазване на профила на групата @@ -2528,6 +3230,10 @@ This cannot be undone! Грешка при запазване на парола в Кeychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Грешка при запазване на настройките @@ -2573,16 +3279,24 @@ This cannot be undone! Грешка при спиране на чата No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Грешка при смяна на профил! - No comment provided by engineer. + alertTitle Error synchronizing connection Грешка при синхронизиране на връзката No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link Грешка при актуализиране на груповия линк @@ -2593,6 +3307,10 @@ This cannot be undone! Грешка при актуализиране на съобщението No comment provided by engineer. + + Error updating server + alert title + Error updating settings Грешка при актуализиране на настройките @@ -2621,7 +3339,9 @@ This cannot be undone! Error: %@ Грешка: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3353,14 @@ This cannot be undone! Грешка: няма файл с база данни No comment provided by engineer. + + Errors + No comment provided by engineer. + + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Дори когато е деактивиран в разговора. @@ -2648,6 +3376,10 @@ This cannot be undone! Разшири chat item action + + Expired + token status text + Export database Експортирай база данни @@ -2658,6 +3390,10 @@ This cannot be undone! Грешка при експортиране: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Експортиран архив на базата данни. @@ -2683,16 +3419,58 @@ This cannot be undone! Бързо и без чакане, докато подателят е онлайн! 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. + No comment provided by engineer. + Favorite Любим + swipe action + + + Favorites No comment provided by engineer. + + File error + file error alert title + + + File errors: +%@ + alert message + + + File is blocked by server operator: +%@. + file error text + + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. Файлът ще бъде изтрит от сървърите. @@ -2713,6 +3491,10 @@ This cannot be undone! Файл: %@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media Файлове и медия @@ -2723,8 +3505,8 @@ This cannot be undone! Файлове и медия chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Файловете и медията са забранени в тази група. No comment provided by engineer. @@ -2793,21 +3575,61 @@ This cannot be undone! Поправката не се поддържа от члена на групата No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + servers error + 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. + No comment provided by engineer. + + + For me + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Препрати chat item action + + Forward %d message(s)? + alert title + Forward and save messages Препращане и запазване на съобщения No comment provided by engineer. + + Forward messages + alert action + + + Forward messages without files? + alert message + + + Forward up to 20 messages at once. + No comment provided by engineer. + Forwarded Препратено @@ -2818,6 +3640,32 @@ This cannot be undone! Препратено от No comment provided by engineer. + + Forwarding %lld messages + 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop Намерено настолно устройство @@ -2838,11 +3686,6 @@ This cannot be undone! Пълно име (незадължително) No comment provided by engineer. - - Full name: - Пълно име: - No comment provided by engineer. - Fully decentralized – visible only to members. Напълно децентрализирана – видима е само за членовете. @@ -2863,6 +3706,18 @@ This cannot be undone! GIF файлове и стикери No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + message preview + + + Good morning! + message preview + Group Група @@ -2918,41 +3773,6 @@ This cannot be undone! Групови линкове No comment provided by engineer. - - Group members can add message reactions. - Членовете на групата могат да добавят реакции към съобщенията. - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. (24 hours) - Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) - No comment provided by engineer. - - - Group members can send SimpleX links. - Членовете на групата могат да изпращат SimpleX линкове. - 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. - Членовете на групата могат да изпращат гласови съобщения. - No comment provided by engineer. - Group message: Групово съобщение: @@ -2993,11 +3813,19 @@ 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 Скрит @@ -3048,10 +3876,17 @@ This cannot be undone! Как работи SimpleX No comment provided by engineer. + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy + No comment provided by engineer. + How it works - Как работи - No comment provided by engineer. + alert button How to @@ -3078,6 +3913,10 @@ This cannot be undone! ICE сървъри (по един на ред) No comment provided by engineer. + + IP address + No comment provided by engineer. + If you can't meet in person, show QR code in a video call, or share the link. Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка. @@ -3118,8 +3957,8 @@ This cannot be undone! Веднага No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Защитен от спам и злоупотреби No comment provided by engineer. @@ -3130,7 +3969,7 @@ This cannot be undone! Import chat database? - Импортиране на чат база данни? + Импортиране на база данни? No comment provided by engineer. @@ -3143,11 +3982,20 @@ This cannot be undone! Неуспешно импортиране No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Импортиране на архив No comment provided by engineer. + + Improved delivery, reduced traffic usage. +More improvements are coming soon! + No comment provided by engineer. + Improved message delivery Подобрена доставка на съобщения @@ -3178,6 +4026,14 @@ This cannot be undone! Звуци по време на разговор No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Инкогнито @@ -3248,6 +4104,11 @@ This cannot be undone! Инсталирайте [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! @@ -3255,16 +4116,35 @@ This cannot be undone! No comment provided by engineer. - - Instantly - Мигновено - No comment provided by engineer. - Interface Интерфейс No comment provided by engineer. + + Interface colors + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code Невалиден QR код @@ -3303,7 +4183,7 @@ This cannot be undone! Invalid server address! Невалиден адрес на сървъра! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4205,10 @@ This cannot be undone! Покани членове No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Покани в групата @@ -3340,8 +4224,8 @@ This cannot be undone! Необратимото изтриване на съобщения е забранено в този чат. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Необратимото изтриване на съобщения е забранено в тази група. No comment provided by engineer. @@ -3366,6 +4250,10 @@ This cannot be undone! 3. Връзката е била компрометирана. No comment provided by engineer. + + It protects your IP address and connections. + 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 (%@). Изглежда, че вече сте свързани чрез този линк. Ако не е така, има грешка (%@). @@ -3384,7 +4272,7 @@ This cannot be undone! Join Присъединяване - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4314,10 @@ This is your link for group %@! Keep Запази + alert action + + + Keep conversation No comment provided by engineer. @@ -3436,7 +4328,7 @@ This is your link for group %@! Keep unused invitation? Запази неизползваната покана за връзка? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4363,14 @@ This is your link for group %@! Leave Напусни + swipe action + + + Leave chat + No comment provided by engineer. + + + Leave chat? No comment provided by engineer. @@ -3513,6 +4413,18 @@ This is your link for group %@! Запомнени настолни устройства No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! Съобщение на живо! @@ -3523,11 +4435,6 @@ This is your link for group %@! Съобщения на живо No comment provided by engineer. - - Local - Локално - No comment provided by engineer. - Local name Локално име @@ -3548,11 +4455,6 @@ This is your link for group %@! Режим на заключване No comment provided by engineer. - - Make a private connection - Добави поверителна връзка - No comment provided by engineer. - Make one message disappear Накарайте едно съобщение да изчезне @@ -3563,21 +4465,11 @@ This is your link for group %@! Направи профила поверителен! 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 Маркирай като изтрито за всички @@ -3603,11 +4495,31 @@ This is your link for group %@! Макс. 30 секунди, получено незабавно. No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member Член No comment provided by engineer. + + Member inactive + item status text + + + Member reports + chat feature + + + 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. Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени. @@ -3618,11 +4530,62 @@ This is your link for group %@! Ролята на члена ще бъде променена на "%@". Членът ще получи нова покана. 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 report messsages to moderators. + No comment provided by engineer. + + + Members can send SimpleX links. + Членовете на групата могат да изпращат SimpleX линкове. + 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. + Членовете на групата могат да изпращат гласови съобщения. + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + No comment provided by engineer. + Message delivery error Грешка при доставката на съобщението @@ -3633,11 +4596,27 @@ This is your link for group %@! Потвърждениe за доставка на съобщения! No comment provided by engineer. + + Message delivery warning + item status text + Message draft Чернова на съобщение No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions Реакции на съобщения @@ -3648,16 +4627,36 @@ This is your link for group %@! Реакциите на съобщения са забранени в този чат. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Реакциите на съобщения са забранени в тази група. 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 + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text Текст на съобщението @@ -3683,6 +4682,22 @@ This is your link for group %@! Съобщенията от %@ ще бъдат показани! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + + + Messages were deleted after you selected them. + alert message + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом. @@ -3748,9 +4763,9 @@ This is your link for group %@! Миграцията е завършена No comment provided by engineer. - - Migrations: %@ - Миграции: %@ + + Migrations: + Миграции: No comment provided by engineer. @@ -3768,6 +4783,10 @@ This is your link for group %@! Модерирано в: %@ copied message info + + More + swipe action + More improvements are coming soon! Очаквайте скоро още подобрения! @@ -3778,16 +4797,15 @@ This is your link for group %@! По-надеждна мрежова връзка. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Най-вероятно тази връзка е изтрита. item status description - - Most likely this contact has deleted the connection with you. - Най-вероятно този контакт е изтрил връзката с вас. - No comment provided by engineer. - Multiple chat profiles Множество профили за чат @@ -3796,7 +4814,11 @@ This is your link for group %@! Mute Без звук - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3806,7 +4828,7 @@ This is your link for group %@! Name Име - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +4840,23 @@ This is your link for group %@! Мрежова връзка No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + snd error text + Network management Управление на мрежата No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Мрежови настройки @@ -3833,16 +4867,32 @@ This is your link for group %@! Състояние на мрежата No comment provided by engineer. + + New + token status text + New Passcode Нов kод за достъп No comment provided by engineer. + + New SOCKS credentials will be used every time you start the app. + No comment provided by engineer. + + + New SOCKS credentials will be used for each server. + No comment provided by engineer. + New chat Нов чат No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request Нова заявка за контакт @@ -3853,11 +4903,6 @@ This is your link for group %@! Нов контакт: notification - - New database archive - Нов архив на база данни - No comment provided by engineer. - New desktop app! Ново настолно приложение! @@ -3868,11 +4913,19 @@ This is your link for group %@! Ново име No comment provided by engineer. + + New events + notification + New in %@ Ново в %@ No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role Нова членска роля @@ -3888,6 +4941,10 @@ This is your link for group %@! Нова парола… No comment provided by engineer. + + New server + No comment provided by engineer. + No Не @@ -3898,6 +4955,18 @@ This is your link for group %@! Приложението няма kод за достъп Authentication unavailable + + 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 Няма избрани контакти @@ -3918,6 +4987,10 @@ This is your link for group %@! Няма токен за устройство! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Няма филтрирани чатове @@ -3933,26 +5006,96 @@ This is your link for group %@! Няма история No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + + + No media & file servers. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + servers error + 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 Няма получени или изпратени файлове No comment provided by engineer. + + 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. + servers error + + + No token! + alert title + + + 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 + No comment provided by engineer. + + + Nothing to forward! + alert title + Notifications Известия @@ -3963,6 +5106,18 @@ This is your link for group %@! Известията са деактивирани! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5135,35 @@ This is your link for group %@! Off Изключено - No comment provided by engineer. + blur media Ok Ок - No comment provided by engineer. + alert button Old database Стара база данни No comment provided by engineer. - - Old database archive - Стар архив на база данни - 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. +Requires compatible VPN. + За свързване ще са **необходими** Onion хостове. +Изисква се активиране на 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. +Requires compatible VPN. + Ще се използват Onion хостове, когато са налични. +Изисква се активиране на VPN. No comment provided by engineer. @@ -4017,11 +5171,19 @@ This is your link for group %@! Няма се използват 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. Само собствениците на групата могат да променят груповите настройки. @@ -4037,6 +5199,14 @@ This is your link for group %@! Само собствениците на групата могат да активират гласови съобщения. 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. Само вие можете да добавяте реакции на съобщенията. @@ -4090,13 +5260,17 @@ This is your link for group %@! Open Отвори - No comment provided by engineer. + alert action Open Settings Отвори настройки No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Отвори чат @@ -4107,31 +5281,41 @@ This is your link for group %@! Отвори конзолата authentication reason + + Open conditions + No comment provided by engineer. + Open group Отвори група No comment provided by engineer. + + Open link? + alert title + Open migration to another device Отвори миграцията към друго устройство authentication reason - - Open user profiles - Отвори потребителските профили - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Протокол и код с отворен код – всеки може да оперира собствени сървъри. - No comment provided by engineer. - Opening app… Приложението се отваря… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + + + Or import archive file + No comment provided by engineer. + Or paste archive link Или постави архивен линк @@ -4152,11 +5336,24 @@ This is your link for group %@! Или покажи този код 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 file errors: +%@ + alert message + PING count PING бройка @@ -4192,6 +5389,10 @@ This is your link for group %@! Кодът за достъп е зададен! No comment provided by engineer. + + Password + No comment provided by engineer. + Password to show Парола за показване @@ -4222,13 +5423,12 @@ This is your link for group %@! Постави получения линк No comment provided by engineer. - - People can connect to you only via the links you share. - Хората могат да се свържат с вас само чрез ликовете, които споделяте. + + Pending No comment provided by engineer. - - Periodically + + Periodic Периодично No comment provided by engineer. @@ -4242,11 +5442,24 @@ This is your link for group %@! Обаждания "картина в картина" 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. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Моля, проверете дали сте използвали правилния линк или поискайте вашия контакт, за да ви изпрати друг. @@ -4314,11 +5527,27 @@ Error: %@ Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите. No comment provided by engineer. + + 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. + token info + Polish interface Полски интерфейс No comment provided by engineer. + + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен @@ -4329,41 +5558,76 @@ Error: %@ Запазете последната чернова на съобщението с прикачени файлове. No comment provided by engineer. - - Preset server - Предварително зададен сървър - No comment provided by engineer. - Preset server address Предварително зададен адрес на сървъра No comment provided by engineer. + + 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 🚀 + No comment provided by engineer. + Private notes Лични бележки name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections Профилни и сървърни връзки @@ -4379,25 +5643,19 @@ Error: %@ Профилни изображения No comment provided by engineer. - - Profile name - Име на профила - No comment provided by engineer. - - - Profile name: - Име на профила: - No comment provided by engineer. - Profile password Профилна парола No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Актуализацията на профила ще бъде изпратена до вашите контакти. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,6 +5677,10 @@ Error: %@ Забрани реакциите на съобщенията. No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. Забранете изпращането на SimpleX линкове. @@ -4444,11 +5706,20 @@ Error: %@ Забрани изпращането на гласови съобщения. No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen Защити екрана на приложението No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Защитете чат профилите с парола! @@ -4464,6 +5735,18 @@ Error: %@ Време за изчакване на протокола за 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 Push известия @@ -4484,6 +5767,10 @@ Error: %@ Оценете приложението No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… Реагирай… @@ -4492,33 +5779,28 @@ Error: %@ Read Прочетено - No comment provided by engineer. + swipe action Read more Прочетете още No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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). No comment provided by engineer. - - Read more in our GitHub repository. - Прочетете повече в нашето хранилище в GitHub. - 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). @@ -4529,6 +5811,10 @@ Error: %@ Потвърждениeто за доставка е деактивирано No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Получено в @@ -4549,16 +5835,23 @@ Error: %@ Получено съобщение message info title + + 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. Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн. No comment provided by engineer. - - Receiving concurrency - Паралелност на получаване - No comment provided by engineer. - Receiving file will be stopped. Получаващият се файл ще бъде спрян. @@ -4584,11 +5877,31 @@ Error: %@ Получателите виждат актуализации, докато ги въвеждате. 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? Повторно свърване със сървърите? @@ -4609,10 +5922,23 @@ Error: %@ Намалена консумация на батерията No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject Отхвърляне - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4639,6 +5965,14 @@ Error: %@ Премахване No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + + + Remove image + No comment provided by engineer. + Remove member Острани член @@ -4699,6 +6033,46 @@ Error: %@ Отговори chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required Задължително @@ -4709,16 +6083,36 @@ Error: %@ Нулиране 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 Рестартирайте приложението, за да създадете нов чат профил @@ -4759,9 +6153,8 @@ Error: %@ Покажи chat item action - - Revert - Отмени промените + + Review conditions No comment provided by engineer. @@ -4789,9 +6182,16 @@ Error: %@ Стартиране на чат No comment provided by engineer. - - SMP servers - SMP сървъри + + SMP server + No comment provided by engineer. + + + SOCKS proxy + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4802,43 +6202,42 @@ Error: %@ Save Запази - chat item action + alert button +chat item action Save (and notify contacts) Запази (и уведоми контактите) - No comment provided by engineer. + alert button Save and notify contact Запази и уведоми контакта - No comment provided by engineer. + alert button 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 Запази паролата и отвори чата @@ -4852,7 +6251,7 @@ Error: %@ Save preferences? Запази настройките? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6266,17 @@ Error: %@ Save servers? Запази сървърите? - No comment provided by engineer. - - - Save settings? - Запази настройките? - No comment provided by engineer. + alert title Save welcome message? Запази съобщението при посрещане? No comment provided by engineer. + + Save your profile? + alert title + Saved Запазено @@ -4899,6 +6297,18 @@ Error: %@ Запазено съобщение message info title + + Saving %lld messages + No comment provided by engineer. + + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Сканирай QR код @@ -4939,11 +6349,19 @@ Error: %@ Търсене или поставяне на SimpleX линк No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Сигурна опашка server test step + + Secured + No comment provided by engineer. + Security assessment Оценка на сигурността @@ -4957,6 +6375,18 @@ Error: %@ Select Избери + chat item action + + + Select chat profile + No comment provided by engineer. + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4994,11 +6424,6 @@ Error: %@ Изпращайте потвърждениe за доставка на No comment provided by engineer. - - Send direct message - Изпрати лично съобщение - No comment provided by engineer. - Send direct message to connect Изпрати лично съобщение за свързване @@ -5009,6 +6434,10 @@ Error: %@ Изпрати изчезващо съобщение No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Изпрати визуализация на линковете @@ -5019,14 +6448,25 @@ Error: %@ Изпрати съобщение на живо 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. + 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. @@ -5052,7 +6492,7 @@ Error: %@ Sender cancelled file transfer. Подателят отмени прехвърлянето на файла. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6549,10 @@ Error: %@ Изпратено на: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Събитие за изпратен файл @@ -5119,11 +6563,59 @@ Error: %@ Изпратено съобщение message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + alert message + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Сървърът изисква оторизация за създаване на опашки, проверете паролата @@ -5139,11 +6631,31 @@ Error: %@ Тестът на сървъра е неуспешен! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + 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 Код на сесията @@ -5154,11 +6666,19 @@ Error: %@ Задай 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 Задай групови настройки @@ -5169,6 +6689,10 @@ Error: %@ Задайте го вместо системната идентификация. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Задай kод за достъп @@ -5199,6 +6723,10 @@ Error: %@ Настройки No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Променете формата на профилните изображения @@ -5207,21 +6735,38 @@ Error: %@ Share Сподели - chat item action + alert action +chat item action Share 1-time link Сподели еднократен линк No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Сподели адрес No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Сподели адреса с контактите? + alert title + + + Share from other apps. No comment provided by engineer. @@ -5229,16 +6774,28 @@ Error: %@ Сподели линк No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Сподели този еднократен линк за връзка No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Сподели с контактите No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code Покажи QR код @@ -5259,21 +6816,41 @@ Error: %@ Показване на последните съобщения в листа с чатовете 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 → on messages sent via private routing. + No comment provided by engineer. + Show: Покажи: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Адрес No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Сигурността на SimpleX Chat беше одитирана от Trail of Bits. @@ -5304,6 +6881,18 @@ Error: %@ SimpleX адрес No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX адрес за контакт @@ -5324,8 +6913,8 @@ Error: %@ SimpleX линкове chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. SimpleX линкове са забранени в тази група. No comment provided by engineer. @@ -5339,11 +6928,19 @@ Error: %@ Еднократна покана за SimpleX simplex link type + + SimpleX protocols reviewed by Trail of Bits. + No comment provided by engineer. + Simplified incognito mode Опростен режим инкогнито No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Пропускане @@ -5359,16 +6956,42 @@ Error: %@ Малки групи (максимум 20) No comment provided by engineer. + + Soft + blur media + + + 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + alert message + Somebody Някой notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. Квадрат, кръг или нещо между тях. @@ -5389,6 +7012,14 @@ Error: %@ Започни миграция No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Спри @@ -5404,11 +7035,6 @@ Error: %@ Спри чата 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. Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян. @@ -5437,28 +7063,56 @@ Error: %@ Stop sharing Спри споделянето - No comment provided by engineer. + alert action Stop sharing address? Спри споделянето на адреса? - No comment provided by engineer. + alert title Stopping chat Спиране на чата No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + blur media + 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 Системен @@ -5469,11 +7123,19 @@ Error: %@ Системна идентификация No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout Времето на изчакване за установяване на TCP връзка No comment provided by engineer. + + TCP port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5489,11 +7151,19 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Направи снимка No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Докосни бутона @@ -5529,16 +7199,19 @@ Error: %@ Докосни за сканиране No comment provided by engineer. - - Tap to start a new chat - Докосни за започване на нов чат - No comment provided by engineer. + + Temporary file error + file error alert title Test failed at step %@. Тестът е неуспешен на стъпка %@. server test failure + + Test notifications + No comment provided by engineer. + Test server Тествай сървър @@ -5552,7 +7225,7 @@ Error: %@ Tests failed! Тестовете са неуспешни! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7242,6 @@ Error: %@ Благодарение на потребителите – допринесете през Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Първата платформа без никакви потребителски идентификатори – поверителна по дизайн. - No comment provided by engineer. - 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. @@ -5586,6 +7254,14 @@ It can happen because of some bug or when the connection is compromised.Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате. 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). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Опитът за промяна на паролата на базата данни не беше завършен. @@ -5596,6 +7272,10 @@ It can happen because of some bug or when the connection is compromised.QR кодът, който сканирахте, не е SimpleX линк за връзка. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Връзката, която приехте, ще бъде отказана! @@ -5616,6 +7296,11 @@ It can happen because of some bug or when the connection is compromised.Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване! No comment provided by engineer. + + The future of messaging + Ново поколение поверителни съобщения + No comment provided by engineer. + The hash of the previous message is different. Хешът на предишното съобщение е различен. @@ -5631,9 +7316,12 @@ It can happen because of some bug or when the connection is compromised.Съобщението ще бъде маркирано като модерирано за всички членове. 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. @@ -5641,9 +7329,12 @@ It can happen because of some bug or when the connection is compromised.Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита. No comment provided by engineer. - - The profile is only shared with your contacts. - Профилът се споделя само с вашите контакти. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! No comment provided by engineer. @@ -5661,14 +7352,25 @@ It can happen because of some bug or when the connection is compromised.Сървърите за нови връзки на текущия ви чат профил **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Текстът, който поставихте, не е SimpleX линк за връзка. No comment provided by engineer. - - Theme - Тема + + 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. @@ -5691,6 +7393,10 @@ It can happen because of some bug or when the connection is compromised.Това действие не може да бъде отменено - съобщенията, изпратени и получени по-рано от избраното, ще бъдат изтрити. Може да отнеме няколко минути. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени. @@ -5736,11 +7442,27 @@ It can happen because of some bug or when the connection is compromised.Това е вашят еднократен линк за връзка! 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. + 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: За да задавате въпроси и да получавате актуализации: @@ -5761,9 +7483,8 @@ It can happen because of some bug or when the connection is compromised.За да направите нова връзка 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. @@ -5771,6 +7492,10 @@ It can happen because of some bug or when the connection is compromised.За да не се разкрива часовата зона, файловете с изображения/глас използват UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,6 +7503,23 @@ You will be prompted to complete authentication before this feature is enabled.< Ще бъдете подканени да извършите идентификация, преди тази функция да бъде активирана. 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. За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон. @@ -5788,26 +7530,54 @@ You will be prompted to complete authentication before this feature is enabled.< За да разкриете своя скрит профил, въведете пълна парола в полето за търсене на страницата **Вашите чат профили**. 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. + + Token status: %@. + token status + + + 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: %@). Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %@). @@ -5863,10 +7633,9 @@ You will be prompted to complete authentication before this feature is enabled.< Отблокирай член? No comment provided by engineer. - - Unexpected error: %@ - Неочаквана грешка: %@ - item status description + + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5876,7 +7645,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. Премахни от любимите - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +7682,10 @@ You will be prompted to complete authentication before this feature is enabled.< Непозната грешка No comment provided by engineer. + + Unknown servers! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Освен ако не използвате интерфейса за повикване на iOS, активирайте режима "Не безпокой", за да избегнете прекъсвания. @@ -5948,11 +7721,15 @@ To connect, please ask your contact to create another connection link and check Unmute Уведомявай - No comment provided by engineer. + notification label action Unread Непрочетено + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5965,11 +7742,6 @@ To connect, please ask your contact to create another connection link and check Актуализация No comment provided by engineer. - - Update .onion hosts setting? - Актуализиране на настройката за .onion хостове? - No comment provided by engineer. - Update database passphrase Актуализирай паролата на базата данни @@ -5980,9 +7752,12 @@ To connect, please ask your contact to create another connection link and check Актуализиране на мрежовите настройки? No comment provided by engineer. - - Update transport isolation mode? - Актуализиране на режима на изолация на транспорта? + + Update settings? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5990,16 +7765,15 @@ To connect, please ask your contact to create another connection link and check Актуализирането на настройките ще свърже отново клиента към всички сървъри. 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 Актуализирай и отвори чата No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Неуспешно качване @@ -6010,21 +7784,45 @@ To connect, please ask your contact to create another connection link and check Качи файл server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Архивът се качва No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Използвай .onion хостове No comment provided by engineer. + + Use SOCKS proxy + No comment provided by engineer. + Use SimpleX Chat servers? Използвай сървърите на SimpleX Chat? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Използвай чата @@ -6035,6 +7833,14 @@ To connect, please ask your contact to create another connection link and check Използвай текущия профил 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 Използвай за нови връзки @@ -6060,24 +7866,46 @@ To connect, please ask your contact to create another connection link and check Използвай само локални известия? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + 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 short links (BETA) + No comment provided by engineer. + Use the app while in the call. Използвайте приложението по време на разговора. No comment provided by engineer. - - User profile - Потребителски профил + + Use the app with one hand. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Използването на .onion хостове изисква съвместим VPN доставчик. + + Use web port + No comment provided by engineer. + + + User selection + No comment provided by engineer. + + + Username No comment provided by engineer. @@ -6150,11 +7978,19 @@ To connect, please ask your contact to create another connection link and check Видео и файлове до 1gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Виж кода за сигурност No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Видима история @@ -6170,8 +8006,8 @@ To connect, please ask your contact to create another connection link and check Гласовите съобщения са забранени в този чат. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Гласовите съобщения са забранени в тази група. No comment provided by engineer. @@ -6210,6 +8046,14 @@ To connect, please ask your contact to create another connection link and check Изчаква се получаването на видеото No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Внимание: стартирането на чата на множество устройства не се поддържа и ще доведе до неуспешно изпращане на съобщения @@ -6255,9 +8099,8 @@ To connect, please ask your contact to create another connection link and check При свързване на аудио и видео разговори. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -6295,24 +8138,34 @@ To connect, please ask your contact to create another connection link and check С намален разход на батерията. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + alert message + Wrong database passphrase Грешна парола за базата данни No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Грешна парола! No comment provided by engineer. - - XFTP servers - XFTP сървъри - No comment provided by engineer. - - - You - Вие + + XFTP server No comment provided by engineer. @@ -6340,6 +8193,10 @@ To connect, please ask your contact to create another connection link and check Вече сте вече свързани с %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Вече се свързвате с %@. @@ -6387,11 +8244,23 @@ Repeat join request? Поканени сте в групата 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 can accept calls from lock screen, without device and app authentication. Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението. No comment provided by engineer. + + 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 Можете да го създадете по-късно @@ -6422,11 +8291,19 @@ Repeat join request? Можете да го направите видим за вашите контакти в SimpleX чрез Настройки. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Вече можете да изпращате съобщения до %@ notification body + + 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. Можете да зададете визуализация на известията на заключен екран през настройките. @@ -6442,16 +8319,15 @@ Repeat join request? Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Можете да включите SimpleX заключване през Настройки. @@ -6465,23 +8341,23 @@ Repeat join request? You can view invitation link again in connection details. Можете да видите отново линкът за покана в подробностите за връзката. - No comment provided by engineer. + alert message You can't send messages! Не може да изпращате съобщения! 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 decide who can connect. + Хората могат да се свържат с вас само чрез ликовете, които споделяте. + No comment provided by engineer. + You have already requested connection via this address! Вече сте заявили връзка през този адрес! @@ -6494,11 +8370,6 @@ Repeat connection request? Изпрати отново заявката за свързване? 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. Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството. @@ -6519,11 +8390,23 @@ Repeat connection request? Вие се присъединихте към тази група. Свързване с поканващия член на групата. 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. Трябва да разрешите на вашия контакт да изпраща гласови съобщения, за да можете да ги изпращате. @@ -6539,6 +8422,10 @@ Repeat connection request? Изпратихте покана за групата No comment provided by engineer. + + You should receive notifications. + token info + You will be connected to group when the group host's device is online, please wait or check later! Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно! @@ -6574,6 +8461,10 @@ Repeat connection request? Все още ще получавате обаждания и известия от заглушени профили, когато са активни. 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. Ще спрете да получавате съобщения от тази група. Историята на чата ще бъде запазена. @@ -6594,29 +8485,14 @@ Repeat connection request? Използвате инкогнито профил за тази група - за да се предотврати споделянето на основния ви профил, поканите на контакти не са разрешени No comment provided by engineer. - - Your %@ servers - Вашите %@ сървъри - No comment provided by engineer. - Your ICE servers Вашите ICE сървъри No comment provided by engineer. - - Your SMP servers - Вашите SMP сървъри - No comment provided by engineer. - Your SimpleX address - Вашият SimpleX адрес - No comment provided by engineer. - - - Your XFTP servers - Вашите XFTP сървъри + Вашият адрес в SimpleX No comment provided by engineer. @@ -6626,24 +8502,25 @@ Repeat connection request? Your chat database - Вашата чат база данни + Вашата база данни No comment provided by engineer. Your chat database is not encrypted - set passphrase to encrypt it. - Вашата чат база данни не е криптирана - задайте парола, за да я криптирате. + Вашата база данни не е криптирана - задайте парола, за да я криптирате. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Вашите чат профили No comment provided by engineer. - - 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). - Вашият контакт трябва да бъде онлайн, за да осъществите връзката. -Можете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк). + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. No comment provided by engineer. @@ -6661,9 +8538,13 @@ You can cancel this connection and remove the contact (and try later with a new Вашите контакти ще останат свързани. No comment provided by engineer. + + Your credentials may be sent unencrypted. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. - Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. + Вашата текуща база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. No comment provided by engineer. @@ -6691,33 +8572,34 @@ You can cancel this connection and remove the contact (and try later with a new Вашият профил **%@** ще бъде споделен. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. -SimpleX сървърите не могат да видят вашия профил. + + Your profile is stored on your device and only shared with your contacts. + Профилът се споделя само с вашите контакти. No comment provided by engineer. - - Your profile, contacts and delivered messages are stored on your device. - Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство. + + 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 was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your random profile Вашият автоматично генериран профил No comment provided by engineer. - - Your server - Вашият сървър - No comment provided by engineer. - Your server address Вашият адрес на сървъра No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Вашите настройки @@ -6758,6 +8640,10 @@ SimpleX сървърите не могат да видят вашия профи обаждането прието call status + + accepted invitation + chat list item title + admin админ @@ -6793,6 +8679,14 @@ SimpleX сървърите не могат да видят вашия профи и %lld други събития No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) аудио разговор (не е e2e криптиран) @@ -6826,13 +8720,18 @@ SimpleX сървърите не могат да видят вашия профи blocked by admin блокиран от админ - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold удебелен No comment provided by engineer. + + call + No comment provided by engineer. + call error грешка при повикване @@ -6936,7 +8835,7 @@ SimpleX сървърите не могат да видят вашия профи connecting… свързване… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +8882,15 @@ SimpleX сървърите не могат да видят вашия профи дни time unit + + decryption errors + No comment provided by engineer. + default (%@) по подразбиране (%@) - pref value + delete after time +pref value default (no) @@ -7010,7 +8914,7 @@ SimpleX сървърите не могат да видят вашия профи deleted group - групата изтрита + групата е изтрита rcv group event chat item @@ -7033,6 +8937,10 @@ SimpleX сървърите не могат да видят вашия профи дублирано съобщение integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e криптиран @@ -7108,9 +9016,8 @@ SimpleX сървърите не могат да видят вашия профи грешка No comment provided by engineer. - - event happened - събитие се случи + + expired No comment provided by engineer. @@ -7143,6 +9050,10 @@ SimpleX сървърите не могат да видят вашия профи iOS Keychain ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на push известия. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link инкогнито чрез линк с адрес за контакт @@ -7183,6 +9094,10 @@ SimpleX сървърите не могат да видят вашия профи покана за група %@ group name + + invite + No comment provided by engineer. + invited поканен @@ -7238,6 +9153,10 @@ SimpleX сървърите не могат да видят вашия профи свързан rcv group event chat item + + message + No comment provided by engineer. + message received получено съобщение @@ -7263,6 +9182,10 @@ SimpleX сървърите не могат да видят вашия профи модерирано от %@ marked deleted chat item preview text + + moderator + member role + months месеци @@ -7271,7 +9194,7 @@ SimpleX сървърите не могат да видят вашия профи never никога - No comment provided by engineer. + delete after time new message @@ -7302,8 +9225,8 @@ SimpleX сървърите не могат да видят вашия профи off изключено enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9243,14 @@ SimpleX сървърите не могат да видят вашия профи включено group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner собственик @@ -7335,6 +9266,14 @@ SimpleX сървърите не могат да видят вашия профи peer-to-peer No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption квантово устойчиво e2e криптиране @@ -7350,6 +9289,10 @@ SimpleX сървърите не могат да видят вашия профи получено потвърждение… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call отхвърлено повикване @@ -7380,6 +9323,10 @@ SimpleX сървърите не могат да видят вашия профи ви острани rcv group event chat item + + requested to connect + chat list item title + saved запазено @@ -7390,6 +9337,10 @@ SimpleX сървърите не могат да видят вашия профи запазено от %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec сек. @@ -7415,6 +9366,12 @@ SimpleX сървърите не могат да видят вашия профи изпрати лично съобщение No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address зададен нов адрес за контакт @@ -7455,11 +9412,19 @@ SimpleX сървърите не могат да видят вашия профи неизвестен connection info + + unknown servers + No comment provided by engineer. + unknown status неизвестен статус No comment provided by engineer. + + unprotected + No comment provided by engineer. + updated group profile актуализиран профил на групата @@ -7500,6 +9465,10 @@ SimpleX сървърите не могат да видят вашия профи чрез реле No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) видео разговор (не е e2e криптиран) @@ -7525,6 +9494,10 @@ SimpleX сървърите не могат да видят вашия профи седмици time unit + + when IP hidden + No comment provided by engineer. + yes да @@ -7609,7 +9582,7 @@ SimpleX сървърите не могат да видят вашия профи
- +
@@ -7646,7 +9619,7 @@ SimpleX сървърите не могат да видят вашия профи
- +
@@ -7666,4 +9639,205 @@ SimpleX сървърите не могат да видят вашия профи
+ +
+ +
+ + + %d new events + notification body + + + From %d chat(s) + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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 + 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. + No comment provided by engineer. + + +
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 @@ ) No comment provided by engineer.
- - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - **Create link / QR code** for your contact to use. 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. + + **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. 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). + + **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. No comment provided by engineer. @@ -217,8 +213,8 @@ **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. + + **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. No comment provided by engineer. @@ -386,8 +382,8 @@ Add servers by scanning QR codes. No comment provided by engineer. - - Add server… + + Add server No comment provided by engineer. @@ -1251,8 +1247,8 @@ Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1271,8 +1267,8 @@ 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. @@ -1751,24 +1747,24 @@ Group links No comment provided by engineer. - - Group members can add message reactions. + + 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. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1899,8 +1895,8 @@ Immediately No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -2020,8 +2016,8 @@ 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. @@ -2207,8 +2203,8 @@ Message reactions are prohibited in this chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. No comment provided by engineer. @@ -2239,8 +2235,8 @@ Migration is completed No comment provided by engineer. - - Migrations: %@ + + Migrations: No comment provided by engineer. @@ -2409,8 +2405,8 @@ 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**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2477,8 +2473,8 @@ Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2537,8 +2533,8 @@ Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -3373,8 +3369,8 @@ Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -3418,16 +3414,16 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of 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. @@ -3490,8 +3486,8 @@ It can happen because of some bug or when the connection is compromised.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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3724,8 +3720,8 @@ To connect, please ask your contact to create another connection link and check 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. @@ -3876,10 +3872,6 @@ To connect, please ask your contact to create another connection link and check You can't send messages! 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. 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 a1ef39b2fb..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 @@
- +
- - - - - - 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) @@ -109,6 +82,7 @@ %@ downloaded + %@ staženo No comment provided by engineer. @@ -126,6 +100,11 @@ %@ je ověřený No comment provided by engineer. + + %@ server + %@ server + No comment provided by engineer. + %@ servers %@ servery @@ -140,6 +119,11 @@ %@ se chce připojit! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members No comment provided by engineer. @@ -159,11 +143,35 @@ %d dní time interval + + %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). + forward confirmation reason + %d hours %d hodin time interval + + %d messages not forwarded + alert title + %d min %d minuty @@ -179,6 +187,10 @@ %d sek time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d přeskočené zprávy @@ -220,14 +232,17 @@ %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. @@ -244,11 +259,6 @@ %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 @@ -299,44 +309,30 @@ %u zpráv přeskočeno. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) No comment provided by engineer. (this device v%@) + (toto zařízení v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Přidat nový kontakt**: pro vytvoření jednorázového QR kódu nebo odkazu pro váš kontakt. + + **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. **Create group**: to create a new group. 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. + + **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). + + **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. @@ -349,11 +345,15 @@ **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. + + **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. + + **Scan / Paste link**: to connect via a link you received. + 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. @@ -378,11 +378,6 @@ \*tučně* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -416,11 +411,6 @@ - historie úprav. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec time to disappear @@ -433,7 +423,8 @@ 1 day 1 den - time interval + delete after time +time interval 1 hour @@ -448,12 +439,26 @@ 1 month 1 měsíc - time interval + delete after time +time interval 1 week 1 týden - time interval + delete after time +time interval + + + 1 year + delete after time + + + 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 @@ -470,11 +475,6 @@ 30 vteřin No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -524,31 +524,29 @@ Přerušit změnu adresy? No comment provided by engineer. - - About SimpleX - O SimpleX - No comment provided by engineer. - About SimpleX Chat O SimpleX chat No comment provided by engineer. - - About SimpleX address - O SimpleX adrese + + About operators No comment provided by engineer. - - Accent color - Zbarvení + + Accent No comment provided by engineer. Accept Přijmout accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + No comment provided by engineer. Accept connection request? @@ -563,20 +561,40 @@ Accept incognito Přijmout inkognito - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + No comment provided by engineer. + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active + token status text + + + 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. 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 contact + + Add friends No comment provided by engineer. - - Add preset servers - Přidejte přednastavené servery + + Add list No comment provided by engineer. @@ -584,14 +602,18 @@ Přidat profil No comment provided by engineer. + + 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ů. No comment provided by engineer. - - Add server… - Přidat server… + + Add team members No comment provided by engineer. @@ -599,11 +621,39 @@ Přidat do jiného zařízení No comment provided by engineer. + + Add to list + No comment provided by engineer. + Add welcome message Přidat uvítací zprávu 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 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address Adresa @@ -614,6 +664,14 @@ Změna adresy bude přerušena. Budou použity staré přijímací adresy. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. No comment provided by engineer. @@ -628,6 +686,14 @@ Pokročilá nastavení sítě No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + + + All + No comment provided by engineer. + All app data is deleted. Všechna data aplikace jsou smazána. @@ -638,16 +704,28 @@ Všechny chaty a zprávy budou smazány – tuto akci nelze vrátit zpět! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + alert message + All data is erased when it is entered. Všechna data se při zadání vymažou. No comment provided by engineer. + + All data is kept private on your device. + No comment provided by engineer. + All group members will remain connected. Všichni členové skupiny zůstanou připojeni. 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. @@ -661,6 +739,18 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + profile dropdown + + + 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. Všechny vaše kontakty zůstanou připojeny. @@ -685,11 +775,19 @@ Povolte hovory, pouze pokud je váš kontakt povolí. No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + 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 downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin) @@ -715,11 +813,19 @@ Povolit odesílání mizících zpráv. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + 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 report messsages to moderators. + No comment provided by engineer. + Allow to send SimpleX links. No comment provided by engineer. @@ -782,6 +888,10 @@ Already joining the group! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay Spojení přes relé @@ -792,11 +902,20 @@ Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle. No comment provided by engineer. + + Another reason + report reason + 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: %@ @@ -811,6 +930,10 @@ Aplikace šifruje nové místní soubory (s výjimkou videí). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon Ikona aplikace @@ -826,6 +949,10 @@ Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem. No comment provided by engineer. + + App session + No comment provided by engineer. + App version Verze aplikace @@ -845,10 +972,46 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + + + Archive + No comment provided by engineer. + + + Archive %lld reports? + 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? + No comment provided by engineer. + + + Archive reports + swipe action + + + Archived contacts + No comment provided by engineer. + Archiving database No comment provided by engineer. @@ -913,11 +1076,19 @@ Automaticky přijímat obrázky No comment provided by engineer. + + Auto-accept settings + alert title + Back Zpět No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -932,15 +1103,51 @@ Špatný hash zprávy 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 Lepší zprávy 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. @@ -969,6 +1176,14 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Vy i váš kontakt můžete přidávat reakce na zprávy. @@ -999,11 +1214,29 @@ Bulharský, finský, thajský a ukrajinský - díky uživatelům a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + 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). Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! Hovor již skončil! @@ -1014,10 +1247,22 @@ Hovory No comment provided by engineer. + + 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! Nelze pozvat kontakt! @@ -1028,10 +1273,15 @@ Nelze pozvat kontakty! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel Zrušit - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1042,10 +1292,18 @@ Nelze získat přístup ke klíčence pro uložení hesla databáze No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Nelze přijmout soubor - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text Cellular @@ -1056,6 +1314,14 @@ Změnit No comment provided by engineer. + + Change automatic message deletion? + alert title + + + Change chat profiles + authentication reason + Change database passphrase? Změnit přístupovou frázi databáze? @@ -1100,11 +1366,22 @@ Change self-destruct passcode Změnit sebedestrukční heslo authentication reason - set passcode view +set passcode view - - Chat archive - Chat se archivuje + + 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. @@ -1122,6 +1399,10 @@ Databáze chatu odstraněna No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported Importovaná databáze chatu @@ -1141,6 +1422,10 @@ 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. @@ -1150,15 +1435,44 @@ Předvolby chatu No comment provided by engineer. + + Chat preferences were changed. + alert message + + + Chat profile + Profil uživatele + 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 Chaty No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Zkontrolujte adresu serveru a zkuste to znovu. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1179,10 +1493,22 @@ Vybrat z knihovny No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear Vyčistit - No comment provided by engineer. + swipe action Clear conversation @@ -1194,6 +1520,14 @@ Vyčistit konverzaci? 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. @@ -1203,11 +1537,18 @@ Zrušte ověření No comment provided by engineer. - - Colors - Barvy + + Color chats with the new themes. No comment provided by engineer. + + Color mode + No comment provided by engineer. + + + Community guidelines violation + report reason + Compare file Porovnat soubor @@ -1218,11 +1559,47 @@ Porovnejte bezpečnostní kódy se svými kontakty. 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 Konfigurace serverů ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Potvrdit @@ -1233,11 +1610,19 @@ Potvrdit heslo No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades Potvrdit aktualizaci databáze No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -1260,6 +1645,10 @@ Confirm upload No comment provided by engineer. + + Confirmed + token status text + Connect Připojit @@ -1278,6 +1667,10 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? No comment provided by engineer. @@ -1310,14 +1703,26 @@ This is your own one-time link! Connect with %@ 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 to server… Připojování k serveru… @@ -1328,6 +1733,10 @@ This is your own one-time link! Připojování k serveru... (chyba: %@) 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. @@ -1337,6 +1746,14 @@ This is your own one-time link! Připojení No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + + + Connection blocked + No comment provided by engineer. + Connection error Chyba připojení @@ -1347,11 +1764,32 @@ This is your own one-time link! Chyba spojení (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + No comment provided by engineer. + + + Connection not ready. + No comment provided by engineer. + + + Connection notifications + No comment provided by engineer. + Connection request sent! Požadavek na připojení byl odeslán! 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. @@ -1361,6 +1799,14 @@ This is your own one-time link! Časový limit připojení No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows Kontakt povolil @@ -1371,6 +1817,10 @@ This is your own one-time link! Kontakt již existuje No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: Skrytý kontakt: @@ -1381,9 +1831,8 @@ This is your own one-time link! Kontakt je připojen notification - - Contact is not connected yet! - Kontakt ještě není připojen! + + Contact is deleted. No comment provided by engineer. @@ -1396,6 +1845,10 @@ This is your own one-time link! Předvolby kontaktů No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Kontakty @@ -1406,21 +1859,37 @@ This is your own one-time link! Kontakty mohou označit zprávy ke smazání; vy je budete moci zobrazit. No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue Pokračovat No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy Kopírovat - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ Verze jádra: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? No comment provided by engineer. @@ -1430,6 +1899,10 @@ This is your own one-time link! Vytvořit No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Vytvořit SimpleX adresu @@ -1439,11 +1912,6 @@ This is your own one-time link! Create a group using a random profile. 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. - No comment provided by engineer. - Create file Vytvořit soubor @@ -1463,6 +1931,10 @@ This is your own one-time link! Vytvořit odkaz No comment provided by engineer. + + Create list + No comment provided by engineer. + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 Vytvořit nový profil v [desktop app](https://simplex.chat/downloads/). 💻 @@ -1470,6 +1942,7 @@ This is your own one-time link! Create profile + Vytvořte si profil No comment provided by engineer. @@ -1487,6 +1960,10 @@ This is your own one-time link! Vytvořte si profil No comment provided by engineer. + + Created + No comment provided by engineer. + Created at No comment provided by engineer. @@ -1495,11 +1972,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - Vytvořeno na %@ - No comment provided by engineer. - Creating archive link No comment provided by engineer. @@ -1513,11 +1985,19 @@ This is your own one-time link! Aktuální heslo 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 passphrase… Aktuální přístupová fráze… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. Aktuálně maximální podporovaná velikost souboru je %@. @@ -1528,11 +2008,23 @@ This is your own one-time link! Vlastní čas No comment provided by engineer. + + Customizable message shape. + No comment provided by engineer. + + + Customize theme + No comment provided by engineer. + Dark Tmavý No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID ID databáze @@ -1631,6 +2123,10 @@ This is your own one-time link! Databáze bude přenesena po restartu aplikace No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized Decentralizované @@ -1644,17 +2140,17 @@ This is your own one-time link! Delete Smazat - chat item action + alert action +swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - Smazat kontakt - No comment provided by engineer. - Delete address Odstranit adresu @@ -1679,14 +2175,12 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. - - Delete archive - Smazat archiv + + Delete chat No comment provided by engineer. - - Delete chat archive? - Smazat archiv chatu? + + Delete chat messages from your device. No comment provided by engineer. @@ -1699,6 +2193,10 @@ This is your own one-time link! Smazat chat profil? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Smazat připojení @@ -1709,9 +2207,8 @@ This is your own one-time link! Smazat kontakt No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1773,6 +2270,10 @@ This cannot be undone! Smazat odkaz? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Smazat zprávu člena? @@ -1786,7 +2287,7 @@ This cannot be undone! Delete messages Smazat zprávy - No comment provided by engineer. + alert button Delete messages after @@ -1803,9 +2304,8 @@ This cannot be undone! Smazat starou databázi? No comment provided by engineer. - - Delete pending connection - Smazat čekající připojení + + Delete or moderate up to 200 messages. No comment provided by engineer. @@ -1823,11 +2323,27 @@ This cannot be undone! Odstranit frontu server test step + + Delete report + No comment provided by engineer. + + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Smazat uživatelský profil? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at Smazáno v @@ -1838,6 +2354,14 @@ This cannot be undone! Smazáno v: %@ copied message info + + Deletion errors + No comment provided by engineer. + + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Doručenka @@ -1870,11 +2394,35 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + 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 Vyvinout No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools Nástroje pro vývojáře @@ -1905,8 +2453,12 @@ This cannot be undone! Přímé zprávy chat feature - - 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. Přímé zprávy mezi členy jsou v této skupině zakázány. No comment provided by engineer. @@ -1920,11 +2472,23 @@ This cannot be undone! Vypnutí zámku SimpleX authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Vypnout pro všechny No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Mizící zpráva @@ -1940,8 +2504,8 @@ This cannot be undone! Mizící zprávy jsou v tomto chatu zakázány. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Mizící zprávy jsou v této skupině zakázány. No comment provided by engineer. @@ -1973,11 +2537,19 @@ This cannot be undone! Discover via local network 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 SimpleX for emergency calls. NEpoužívejte SimpleX pro tísňová volání. No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later Udělat později @@ -1987,6 +2559,14 @@ This cannot be undone! Do not send history to new members. No comment provided by engineer. + + Do not use credentials with proxy. + No comment provided by engineer. + + + Documents: + No comment provided by engineer. + Don't create address Nevytvářet adresu @@ -1997,11 +2577,19 @@ This cannot be undone! Nepovolovat No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Znovu neukazuj No comment provided by engineer. + + Done + No comment provided by engineer. + Downgrade and open chat Snížit a otevřít chat @@ -2009,7 +2597,12 @@ This cannot be undone! Download - chat item action + alert button +chat item action + + + Download errors + No comment provided by engineer. Download failed @@ -2020,6 +2613,18 @@ This cannot be undone! Stáhnout soubor server test step + + Download files + alert action + + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive No comment provided by engineer. @@ -2038,6 +2643,10 @@ This cannot be undone! Trvání No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Upravit @@ -2058,6 +2667,10 @@ This cannot be undone! Povolit (zachovat přepsání) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock Zapnutí zámku SimpleX @@ -2071,7 +2684,7 @@ This cannot be undone! Enable automatic message deletion? Povolit automatické mazání zpráv? - No comment provided by engineer. + alert title Enable camera access @@ -2116,6 +2729,10 @@ This cannot be undone! Povolit sebedestrukční heslo set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2187,6 +2804,10 @@ This cannot be undone! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Zadat heslo @@ -2248,26 +2869,33 @@ This cannot be undone! Chyba přerušení změny adresy No comment provided by engineer. + + Error accepting conditions + alert title + 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(ů) No comment provided by engineer. + + Error adding server + alert title + Error changing address Chuba změny adresy No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Chyba při změně role @@ -2278,6 +2906,18 @@ This cannot be undone! Chyba změny nastavení 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 Chyba při vytváření adresy @@ -2293,6 +2933,10 @@ This cannot be undone! Chyba při vytváření odkazu skupiny No comment provided by engineer. + + Error creating list + alert title + Error creating member contact Chyba vytvoření kontaktu člena @@ -2307,6 +2951,10 @@ This cannot be undone! Chyba při vytváření profilu! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Chyba dešifrování souboru @@ -2327,11 +2975,6 @@ This cannot be undone! 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 @@ -2376,6 +3019,10 @@ This cannot be undone! Chyba při exportu databáze chatu No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Chyba při importu databáze chatu @@ -2386,9 +3033,12 @@ This cannot be undone! Chyba při připojování ke skupině No comment provided by engineer. - - Error loading %@ servers - Chyba načítání %@ serverů + + Error loading servers + alert title + + + Error migrating settings No comment provided by engineer. @@ -2398,16 +3048,31 @@ This cannot be undone! Error receiving file Chyba při příjmu souboru + alert title + + + Error reconnecting server No comment provided by engineer. + + Error reconnecting servers + No comment provided by engineer. + + + Error registering for notifications + alert title + Error removing member Chyba při odebrání člena No comment provided by engineer. - - Error saving %@ servers - Chyba při ukládání serverů %@ + + Error reordering lists + alert title + + + Error resetting statistics No comment provided by engineer. @@ -2415,6 +3080,10 @@ This cannot be undone! Chyba při ukládání serverů ICE No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile Chyba při ukládání profilu skupiny @@ -2430,6 +3099,10 @@ This cannot be undone! Při ukládání přístupové fráze do klíčenky došlo k chybě No comment provided by engineer. + + Error saving servers + alert title + Error saving settings when migrating @@ -2473,16 +3146,24 @@ This cannot be undone! Chyba při zastavení chatu No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Chyba při přepínání profilu! - No comment provided by engineer. + alertTitle Error synchronizing connection Chyba synchronizace připojení No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link Chyba aktualizace odkazu skupiny @@ -2493,6 +3174,10 @@ This cannot be undone! Chyba aktualizace zprávy No comment provided by engineer. + + Error updating server + alert title + Error updating settings Chyba při aktualizaci nastavení @@ -2519,7 +3204,9 @@ This cannot be undone! Error: %@ Chyba: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2531,6 +3218,14 @@ This cannot be undone! Chyba: žádný soubor databáze No comment provided by engineer. + + Errors + No comment provided by engineer. + + + Errors in servers configuration. + servers error + Even when disabled in the conversation. I při vypnutí v konverzaci. @@ -2545,6 +3240,10 @@ This cannot be undone! Expand chat item action + + Expired + token status text + Export database Export databáze @@ -2555,6 +3254,10 @@ This cannot be undone! Chyba exportu: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Exportovaný archiv databáze. @@ -2579,15 +3282,57 @@ This cannot be undone! Rychle a bez čekání, než bude odesílatel 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. + No comment provided by engineer. + Favorite Oblíbené + swipe action + + + Favorites No comment provided by engineer. + + File error + file error alert title + + + File errors: +%@ + alert message + + + File is blocked by server operator: +%@. + file error text + + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. Soubor bude smazán ze serverů. @@ -2608,6 +3353,10 @@ This cannot be undone! Soubor: %@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media Soubory a média @@ -2618,8 +3367,8 @@ This cannot be undone! Soubory a média chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Soubory a média jsou zakázány v této skupině. No comment provided by engineer. @@ -2685,19 +3434,59 @@ This cannot be undone! Opravit nepodporované členem skupiny No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + servers error + For console Pro konzoli 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. + No comment provided by engineer. + + + For me + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward chat item action + + Forward %d message(s)? + alert title + Forward and save messages No comment provided by engineer. + + Forward messages + alert action + + + Forward messages without files? + alert message + + + Forward up to 20 messages at once. + No comment provided by engineer. + Forwarded No comment provided by engineer. @@ -2706,6 +3495,32 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding %lld messages + 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop No comment provided by engineer. @@ -2725,11 +3540,6 @@ This cannot be undone! Celé jméno (volitelně) No comment provided by engineer. - - Full name: - Celé jméno: - No comment provided by engineer. - Fully decentralized – visible only to members. No comment provided by engineer. @@ -2749,6 +3559,18 @@ This cannot be undone! GIFy a nálepky No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + message preview + + + Good morning! + message preview + Group Skupina @@ -2802,40 +3624,6 @@ This cannot be undone! 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. (24 hours) - Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin) - No comment provided by engineer. - - - Group members can send SimpleX links. - 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. - No comment provided by engineer. - Group message: Skupinová zpráva: @@ -2876,11 +3664,19 @@ This cannot be undone! Skupina bude smazána pro vás - toto nelze vzít zpět! No comment provided by engineer. + + Groups + No comment provided by engineer. + Help Pomoc No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Skryté @@ -2930,10 +3726,17 @@ This cannot be undone! Jak SimpleX funguje No comment provided by engineer. + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy + No comment provided by engineer. + How it works - Jak to funguje - No comment provided by engineer. + alert button How to @@ -2959,6 +3762,10 @@ This cannot be undone! Servery ICE (jeden na řádek) No comment provided by engineer. + + IP address + 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. @@ -2999,8 +3806,8 @@ This cannot be undone! Ihned No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Odolná vůči spamu a zneužití No comment provided by engineer. @@ -3023,10 +3830,19 @@ This cannot be undone! 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. +More improvements are coming soon! + No comment provided by engineer. + Improved message delivery No comment provided by engineer. @@ -3054,6 +3870,14 @@ This cannot be undone! In-call sounds No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Inkognito @@ -3122,6 +3946,11 @@ This cannot be undone! Nainstalujte [SimpleX Chat pro terminál](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Okamžitě + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3129,16 +3958,35 @@ This cannot be undone! No comment provided by engineer. - - Instantly - Okamžitě - No comment provided by engineer. - Interface Rozhranní No comment provided by engineer. + + Interface colors + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code No comment provided by engineer. @@ -3171,7 +4019,7 @@ This cannot be undone! Invalid server address! Neplatná adresa serveru! - No comment provided by engineer. + alert title Invalid status @@ -3193,6 +4041,10 @@ This cannot be undone! Pozvat členy No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Pozvat do skupiny @@ -3208,8 +4060,8 @@ This cannot be undone! 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. + + Irreversible message deletion is prohibited. Nevratné mazání zpráv je v této skupině zakázáno. No comment provided by engineer. @@ -3234,6 +4086,10 @@ This cannot be undone! 3. Spojení je kompromitováno. No comment provided by engineer. + + It protects your IP address and connections. + 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 (%@). Zdá se, že jste již připojeni prostřednictvím tohoto odkazu. Pokud tomu tak není, došlo k chybě (%@). @@ -3252,7 +4108,7 @@ This cannot be undone! Join Připojte se na - No comment provided by engineer. + swipe action Join group @@ -3288,6 +4144,10 @@ This is your link for group %@! Keep + alert action + + + Keep conversation No comment provided by engineer. @@ -3296,7 +4156,7 @@ This is your link for group %@! Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3331,6 +4191,14 @@ This is your link for group %@! Leave Opustit + swipe action + + + Leave chat + No comment provided by engineer. + + + Leave chat? No comment provided by engineer. @@ -3370,6 +4238,18 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! Živé zprávy! @@ -3380,11 +4260,6 @@ This is your link for group %@! Živé zprávy No comment provided by engineer. - - Local - Místní - No comment provided by engineer. - Local name Místní název @@ -3405,11 +4280,6 @@ This is your link for group %@! 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 @@ -3420,21 +4290,11 @@ This is your link for group %@! 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 @@ -3460,11 +4320,31 @@ This is your link for group %@! Max 30 vteřin, přijato okamžitě. No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member Člen No comment provided by engineer. + + Member inactive + item status text + + + Member reports + chat feature + + + 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. Role člena se změní na "%@". Všichni členové skupiny budou upozorněni. @@ -3475,11 +4355,61 @@ This is your link for group %@! Role člena se změní na "%@". Člen obdrží novou pozvánku. 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! Č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 report messsages to moderators. + No comment provided by engineer. + + + Members can send SimpleX links. + 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. + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + No comment provided by engineer. + Message delivery error Chyba doručení zprávy @@ -3490,11 +4420,27 @@ This is your link for group %@! Potvrzení o doručení zprávy! No comment provided by engineer. + + Message delivery warning + item status text + Message draft Návrh zprávy No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions Reakce na zprávy @@ -3505,15 +4451,35 @@ This is your link for group %@! Reakce na zprávy jsou v tomto chatu zakázány. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Reakce na zprávy jsou v této skupině zakázány. 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 + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text Text zprávy @@ -3537,6 +4503,22 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + + + Messages were deleted after you selected them. + alert message + 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. @@ -3593,9 +4575,9 @@ This is your link for group %@! Přenesení dokončeno No comment provided by engineer. - - Migrations: %@ - Migrace: %@ + + Migrations: + Migrace: No comment provided by engineer. @@ -3613,6 +4595,10 @@ This is your link for group %@! Upraveno v: %@ copied message info + + More + swipe action + More improvements are coming soon! Další vylepšení se chystají již brzy! @@ -3622,16 +4608,15 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Pravděpodobně je toto spojení smazáno. item status description - - 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ů @@ -3640,7 +4625,11 @@ This is your link for group %@! Mute Ztlumit - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3650,7 +4639,7 @@ This is your link for group %@! Name Jméno - No comment provided by engineer. + swipe action Network & servers @@ -3661,10 +4650,22 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + snd error text + Network management No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Nastavení sítě @@ -3675,15 +4676,31 @@ This is your link for group %@! Stav sítě No comment provided by engineer. + + New + token status text + New Passcode Nové heslo No comment provided by engineer. + + New SOCKS credentials will be used every time you start the app. + No comment provided by engineer. + + + New SOCKS credentials will be used for each server. + No comment provided by engineer. + New chat No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request Žádost o nový kontakt @@ -3694,11 +4711,6 @@ This is your link for group %@! Nový kontakt: notification - - New database archive - Archiv nové databáze - No comment provided by engineer. - New desktop app! Nová desktopová aplikace! @@ -3709,11 +4721,19 @@ This is your link for group %@! Nově zobrazované jméno No comment provided by engineer. + + New events + notification + New in %@ Nový V %@ No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role Nová role člena @@ -3729,6 +4749,10 @@ This is your link for group %@! Nová přístupová fráze… No comment provided by engineer. + + New server + No comment provided by engineer. + No Ne @@ -3739,6 +4763,18 @@ This is your link for group %@! Žádné heslo aplikace Authentication unavailable + + 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 Nebyl vybrán žádný kontakt @@ -3759,6 +4795,10 @@ This is your link for group %@! Žádný token zařízení! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Žádné filtrované chaty @@ -3774,24 +4814,94 @@ This is your link for group %@! Žádná historie No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + + + No media & file servers. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + servers error + 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 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 No comment provided by engineer. + + 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. + servers error + + + No token! + alert title + + + No unread chats + No comment provided by engineer. + + + No user identifiers. + Bez uživatelských identifikátorů + No comment provided by engineer. + Not compatible! No comment provided by engineer. + + Notes + No comment provided by engineer. + + + Nothing selected + No comment provided by engineer. + + + Nothing to forward! + alert title + Notifications Oznámení @@ -3802,6 +4912,18 @@ This is your link for group %@! Oznámení jsou zakázána! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3818,36 +4940,35 @@ This is your link for group %@! Off Vypnout - No comment provided by engineer. + blur media Ok Ok - No comment provided by engineer. + alert button Old database Stará databáze No comment provided by engineer. - - Old database archive - Archiv staré databáze - No comment provided by engineer. - 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. +Requires compatible VPN. + Pro připojení budou vyžadováni Onion hostitelé. +Vyž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. +Requires compatible VPN. + Onion hostitelé budou použiti, pokud jsou k dispozici. +Vyžaduje povolení sítě VPN. No comment provided by engineer. @@ -3855,11 +4976,19 @@ This is your link for group %@! 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**. + + Only chat owners can change preferences. + No comment provided by engineer. + + + 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 delete conversation + No comment provided by engineer. + Only group owners can change group preferences. Předvolby skupiny mohou měnit pouze vlastníci skupiny. @@ -3875,6 +5004,14 @@ This is your link for group %@! Pouze majitelé skupin mohou povolit zasílání hlasových zpráv. 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. Reakce na zprávy můžete přidávat pouze vy. @@ -3928,13 +5065,17 @@ This is your link for group %@! Open Otevřít - No comment provided by engineer. + alert action Open Settings Otevřít nastavení No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Otevřete chat @@ -3945,28 +5086,38 @@ This is your link for group %@! Otevřete konzolu chatu authentication reason + + Open conditions + No comment provided by engineer. + Open group No comment provided by engineer. + + Open link? + alert title + Open migration to another device authentication reason - - Open user profiles - Otevřít uživatelské profily - authentication reason - - - 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 app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + + + Or import archive file + No comment provided by engineer. + Or paste archive link No comment provided by engineer. @@ -3983,10 +5134,23 @@ This is your link for group %@! 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 file errors: +%@ + alert message + PING count Počet PING @@ -4022,6 +5186,10 @@ This is your link for group %@! Heslo nastaveno! No comment provided by engineer. + + Password + No comment provided by engineer. + Password to show Heslo k zobrazení @@ -4048,13 +5216,12 @@ This is your link for group %@! Paste the link you received 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. + + Pending No comment provided by engineer. - - Periodically + + Periodic Pravidelně No comment provided by engineer. @@ -4067,11 +5234,24 @@ This is your link for group %@! Picture-in-picture calls 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. Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv. 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. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Zkontrolujte, zda jste použili správný odkaz, nebo požádejte kontakt, aby vám poslal jiný. @@ -4136,11 +5316,27 @@ Error: %@ Heslo uložte bezpečně, v případě jeho ztráty jej NEBUDE možné změnit. No comment provided by engineer. + + 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. + token info + Polish interface Polské rozhraní No comment provided by engineer. + + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Je možné, že otisk certifikátu v adrese serveru je nesprávný @@ -4151,40 +5347,75 @@ Error: %@ 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 No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Náhled No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Ochrana osobních údajů a zabezpečení 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 Nové vymezení soukromí No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + No comment provided by engineer. + Private filenames Soukromé názvy souborů 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 🚀 + No comment provided by engineer. + Private notes name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections Profil a připojení k serveru @@ -4199,23 +5430,19 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - No comment provided by engineer. - Profile password Heslo profilu No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Aktualizace profilu bude zaslána vašim kontaktům. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4237,6 +5464,10 @@ Error: %@ Zakázat reakce na zprávy. No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. No comment provided by engineer. @@ -4261,11 +5492,20 @@ Error: %@ Zakázat odesílání hlasových zpráv. No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen Ochrana obrazovky aplikace No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Chraňte své chat profily heslem! @@ -4281,6 +5521,18 @@ Error: %@ Časový limit protokolu na 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 Nabízená oznámení @@ -4299,6 +5551,10 @@ Error: %@ Ohodnoťte aplikaci No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… Reagovat… @@ -4307,32 +5563,27 @@ Error: %@ Read Číst - No comment provided by engineer. + swipe action 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). - No comment provided by engineer. - Read more in [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). + Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Přečtěte si více v [Uživatelské příručce](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Další informace najdete v našem repozitáři GitHub. - 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). @@ -4343,6 +5594,10 @@ Error: %@ Informace o dodání jsou zakázány No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Přijato v @@ -4363,15 +5618,23 @@ Error: %@ Přijatá zpráva message info title + + 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. Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele. No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. Příjem souboru bude zastaven. @@ -4395,11 +5658,31 @@ Error: %@ Příjemci uvidí aktualizace během jejich psaní. No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Znovu připojte všechny připojené servery a vynuťte doručení zprávy. Využívá další provoz. No comment provided by engineer. + + Reconnect 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? Znovu připojit servery? @@ -4420,10 +5703,23 @@ Error: %@ Snížení spotřeby baterie No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject Odmítnout - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4450,6 +5746,14 @@ Error: %@ Odstranit No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + + + Remove image + No comment provided by engineer. + Remove member Odstranit člena @@ -4505,6 +5809,46 @@ Error: %@ Odpověď chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required Povinné @@ -4515,16 +5859,36 @@ Error: %@ Obnovit 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 Obnovení barev No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Obnovení výchozího nastavení No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Restartujte aplikaci pro vytvoření nového chat profilu @@ -4564,9 +5928,8 @@ Error: %@ Odhalit chat item action - - Revert - Vrátit + + Review conditions No comment provided by engineer. @@ -4594,9 +5957,16 @@ Error: %@ Spustit chat No comment provided by engineer. - - SMP servers - SMP servery + + SMP server + No comment provided by engineer. + + + SOCKS proxy + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4606,43 +5976,42 @@ Error: %@ Save Uložit - chat item action + alert button +chat item action 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. + alert button Save and notify group members Uložit a upozornit členy skupiny No comment provided by engineer. + + Save and reconnect + 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 No comment provided by engineer. + + Save list + No comment provided by engineer. + Save passphrase and open chat Uložte heslo a otevřete chat @@ -4656,7 +6025,7 @@ Error: %@ Save preferences? Uložit předvolby? - No comment provided by engineer. + alert title Save profile password @@ -4671,18 +6040,17 @@ Error: %@ Save servers? Uložit servery? - No comment provided by engineer. - - - Save settings? - Uložit nastavení? - No comment provided by engineer. + alert title Save welcome message? Uložit uvítací zprávu? No comment provided by engineer. + + Save your profile? + alert title + Saved No comment provided by engineer. @@ -4700,6 +6068,18 @@ Error: %@ Saved message message info title + + Saving %lld messages + No comment provided by engineer. + + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Skenovat QR kód @@ -4737,11 +6117,19 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Zabezpečit frontu server test step + + Secured + No comment provided by engineer. + Security assessment Posouzení bezpečnosti @@ -4755,6 +6143,18 @@ Error: %@ Select Vybrat + chat item action + + + Select chat profile + No comment provided by engineer. + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4792,11 +6192,6 @@ Error: %@ Potvrzení o doručení zasílat na 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í @@ -4807,6 +6202,10 @@ Error: %@ Poslat mizící zprávu No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Odesílání náhledů odkazů @@ -4817,14 +6216,25 @@ Error: %@ Odeslat živou zprávu 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. + 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 Odeslat oznámení No comment provided by engineer. - - Send notifications: - Odeslat oznámení: + + Send private reports No comment provided by engineer. @@ -4849,7 +6259,7 @@ Error: %@ Sender cancelled file transfer. Odesílatel zrušil přenos souboru. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -4906,6 +6316,10 @@ Error: %@ Posláno v: % @ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Odeslaná událost souboru @@ -4916,11 +6330,59 @@ Error: %@ Poslaná zpráva message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. Odeslané zprávy se po uplynutí nastavené doby odstraní. 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 + No comment provided by engineer. + + + Server added to operator %@. + alert message + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo @@ -4936,11 +6398,31 @@ Error: %@ Test serveru se nezdařil! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers Servery 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. @@ -4950,11 +6432,19 @@ Error: %@ Nastavit 1 den No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… Nastavení jména kontaktu… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Nastavení skupinových předvoleb @@ -4965,6 +6455,10 @@ Error: %@ Nastavte jej namísto ověřování systému. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Nastavit heslo @@ -4994,6 +6488,10 @@ Error: %@ Nastavení No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images No comment provided by engineer. @@ -5001,21 +6499,38 @@ Error: %@ Share Sdílet - chat item action + alert action +chat item action Share 1-time link Sdílet jednorázovou pozvánku No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Sdílet adresu No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Sdílet adresu s kontakty? + alert title + + + Share from other apps. No comment provided by engineer. @@ -5023,15 +6538,27 @@ Error: %@ Sdílet odkaz No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Sdílet s kontakty No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -5051,21 +6578,41 @@ Error: %@ Zobrazit poslední zprávy No comment provided by engineer. + + Show message status + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview Zobrazení náhledu No comment provided by engineer. + + Show → on messages sent via private routing. + No comment provided by engineer. + Show: Zobrazit: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Adresa No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Zabezpečení SimpleX chatu bylo auditováno společností Trail of Bits. @@ -5096,6 +6643,18 @@ Error: %@ Adresa SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX kontaktní adresa @@ -5116,8 +6675,8 @@ Error: %@ Odkazy na SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. No comment provided by engineer. @@ -5129,11 +6688,19 @@ Error: %@ Jednorázová pozvánka SimpleX simplex link type + + SimpleX protocols reviewed by Trail of Bits. + No comment provided by engineer. + Simplified incognito mode Zjednodušený inkognito režim No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Přeskočit @@ -5149,16 +6716,42 @@ Error: %@ Malé skupiny (max. 20) No comment provided by engineer. + + Soft + blur media + + + 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. Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli. No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + + + Some servers failed the test: +%@ + alert message + Somebody Někdo notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. No comment provided by engineer. @@ -5177,6 +6770,14 @@ Error: %@ Zahájit přenesení No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Zastavit @@ -5191,11 +6792,6 @@ Error: %@ Stop chat 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. @@ -5224,27 +6820,55 @@ Error: %@ Stop sharing Přestat sdílet - No comment provided by engineer. + alert action Stop sharing address? Přestat sdílet adresu? - No comment provided by engineer. + alert title Stopping chat No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + blur media + Submit Odeslat 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 Podpořte 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 Systém @@ -5255,11 +6879,19 @@ Error: %@ Ověření systému No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout Časový limit připojení TCP No comment provided by engineer. + + TCP port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5275,11 +6907,19 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Vyfotit No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Klepněte na tlačítko @@ -5312,16 +6952,19 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - Klepnutím na zahájíte nový chat - No comment provided by engineer. + + Temporary file error + file error alert title Test failed at step %@. Test selhal v kroku %@. server test failure + + Test notifications + No comment provided by engineer. + Test server Testovací server @@ -5335,7 +6978,7 @@ Error: %@ Tests failed! Testy selhaly! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5352,11 +6995,6 @@ Error: %@ 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 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. @@ -5369,6 +7007,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Aplikace vás může upozornit na přijaté zprávy nebo žádosti o kontakt - povolte to v nastavení. 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). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Pokus o změnu přístupové fráze databáze nebyl dokončen. @@ -5378,6 +7024,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Připojení, které jste přijali, bude zrušeno! @@ -5398,6 +7048,11 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Šifrování funguje a nové povolení šifrování není vyžadováno. To může vyvolat chybu v připojení! No comment provided by engineer. + + The future of messaging + Nová generace soukromých zpráv + No comment provided by engineer. + The hash of the previous message is different. Hash předchozí zprávy se liší. @@ -5413,9 +7068,12 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován 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 + + 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. @@ -5423,9 +7081,12 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Stará databáze nebyla během přenášení odstraněna, lze ji smazat. No comment provided by engineer. - - The profile is only shared with your contacts. - Profil je sdílen pouze s vašimi kontakty. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! No comment provided by engineer. @@ -5443,13 +7104,24 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Servery pro nová připojení vašeho aktuálního chat profilu **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - Téma + + 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. @@ -5472,6 +7144,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován 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. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny. @@ -5511,11 +7187,27 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován This is your own one-time link! 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. + 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 **%@**. Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**. No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: Chcete-li položit jakékoli dotazy a dostávat aktuality: @@ -5535,9 +7227,8 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován 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ů. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -5545,6 +7236,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován K ochraně časového pásma používají obrazové/hlasové soubory UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5552,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í. 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 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. Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu. @@ -5562,26 +7274,54 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Chat profily**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze. 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. Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních. No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. Změnit inkognito režim při připojení. No comment provided by engineer. + + Token status: %@. + token status + + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation Izolace transportu 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: %@). Pokus o připojení k serveru používanému k přijímání zpráv od tohoto kontaktu (chyba: %@). @@ -5631,10 +7371,9 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unblock member? No comment provided by engineer. - - Unexpected error: %@ - Neočekávaná chyba: %@ - item status description + + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5644,7 +7383,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unfav. Odobl. - No comment provided by engineer. + swipe action Unhide @@ -5681,6 +7420,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Neznámá chyba No comment provided by engineer. + + Unknown servers! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Při nepoužívání rozhraní volání iOS, povolte režim Nerušit, abyste se vyhnuli vyrušování. @@ -5714,11 +7457,15 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Unmute Zrušit ztlumení - No comment provided by engineer. + notification label action Unread Nepřečtený + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5730,11 +7477,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu 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 @@ -5745,9 +7487,12 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Aktualizovat nastavení sítě? No comment provided by engineer. - - Update transport isolation mode? - Aktualizovat režim dopravní izolace? + + Update settings? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5755,16 +7500,15 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu 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 No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5774,20 +7518,44 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Nahrát soubor server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Použít hostitele .onion No comment provided by engineer. + + Use SOCKS proxy + No comment provided by engineer. + Use SimpleX Chat servers? Používat servery SimpleX Chat? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Použijte chat @@ -5798,6 +7566,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Použít aktuální profil 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 Použít pro nová připojení @@ -5821,23 +7597,45 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + No comment provided by engineer. + Use server Použít server No comment provided by engineer. + + Use servers + No comment provided by engineer. + + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. - - User profile - Profil uživatele + + Use the app with one hand. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Použití hostitelů .onion vyžaduje kompatibilního poskytovatele VPN. + + Use web port + No comment provided by engineer. + + + User selection + No comment provided by engineer. + + + Username No comment provided by engineer. @@ -5904,11 +7702,19 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Videa a soubory až do velikosti 1 gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Zobrazení bezpečnostního kódu No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history chat feature @@ -5923,8 +7729,8 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Hlasové zprávy jsou v tomto chatu zakázány. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Hlasové zprávy jsou v této skupině zakázány. No comment provided by engineer. @@ -5961,6 +7767,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Čekám na video No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + 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. @@ -6003,9 +7817,8 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu When connecting audio and video calls. 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. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -6038,24 +7851,34 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + alert message + Wrong database passphrase Špatná přístupová fráze k databázi No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Špatná přístupová fráze! No comment provided by engineer. - - XFTP servers - XFTP servery - No comment provided by engineer. - - - You - Vy + + XFTP server No comment provided by engineer. @@ -6082,6 +7905,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Již jste připojeni k %@. 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. @@ -6121,11 +7948,23 @@ Repeat join request? Jste pozváni do skupiny 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 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 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 Můžete vytvořit později @@ -6154,11 +7993,19 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Nyní můžete posílat zprávy %@ notification body + + 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. Náhled oznámení na zamykací obrazovce můžete změnit v nastavení. @@ -6174,16 +8021,15 @@ Repeat join request? 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 No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Zámek SimpleX můžete zapnout v Nastavení. @@ -6196,23 +8042,23 @@ Repeat join request? You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! Nemůžete posílat zprávy! 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 decide who can connect. + Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte. + No comment provided by engineer. + You have already requested connection via this address! No comment provided by engineer. @@ -6222,11 +8068,6 @@ Repeat join request? Repeat connection request? No comment provided by engineer. - - You have no chats - Nemáte žádné konverzace - 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í. @@ -6247,11 +8088,23 @@ Repeat connection request? Připojili jste se k této skupině. Připojení k pozvání člena skupiny. 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. 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ů. 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. Abyste mohli odesílat hlasové zprávy, musíte je povolit svému kontaktu. @@ -6267,6 +8120,10 @@ Repeat connection request? Odeslali jste pozvánku do skupiny No comment provided by engineer. + + You should receive notifications. + token info + You will be connected to group when the group host's device is online, please wait or check later! Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později! @@ -6300,6 +8157,10 @@ Repeat connection request? Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + 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. @@ -6320,31 +8181,16 @@ Repeat connection request? 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 ICE servers Vaše servery ICE No comment provided by engineer. - - Your SMP servers - Vaše servery SMP - No comment provided by engineer. - Your SimpleX address Vaše SimpleX adresa No comment provided by engineer. - - Your XFTP servers - Vaše XFTP servery - No comment provided by engineer. - Your calls Vaše hovory @@ -6360,16 +8206,17 @@ Repeat connection request? Vaše chat databáze není šifrována – nastavte přístupovou frázi pro její šifrování. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Vaše chat profily No comment provided by engineer. - - 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). - 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). + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. No comment provided by engineer. @@ -6387,6 +8234,10 @@ Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s nov Vaše kontakty zůstanou připojeny. No comment provided by engineer. + + Your credentials may be sent unencrypted. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Vaše aktuální chat databáze bude ODSTRANĚNA a NAHRAZENA importovanou. @@ -6416,33 +8267,34 @@ Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s nov Váš profil **%@** bude sdílen. No comment provided by engineer. - - 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. + + 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. - - 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í. + + 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 was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + 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 No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Vaše nastavení @@ -6483,6 +8335,10 @@ Servery SimpleX nevidí váš profil. přijatý hovor call status + + accepted invitation + chat list item title + admin správce @@ -6515,6 +8371,14 @@ Servery SimpleX nevidí váš profil. and %lld other events No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) zvukový hovor (nešifrovaný e2e) @@ -6544,13 +8408,18 @@ Servery SimpleX nevidí váš profil. blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold tučně No comment provided by engineer. + + call + No comment provided by engineer. + call error chyba volání @@ -6654,7 +8523,7 @@ Servery SimpleX nevidí váš profil. connecting… připojení… - chat list item title + No comment provided by engineer. connection established @@ -6700,10 +8569,15 @@ Servery SimpleX nevidí váš profil. dní time unit + + decryption errors + No comment provided by engineer. + default (%@) výchozí (%@) - pref value + delete after time +pref value default (no) @@ -6749,6 +8623,10 @@ Servery SimpleX nevidí váš profil. duplicitní zpráva integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e šifrované @@ -6824,8 +8702,8 @@ Servery SimpleX nevidí váš profil. chyba No comment provided by engineer. - - event happened + + expired No comment provided by engineer. @@ -6857,6 +8735,10 @@ Servery SimpleX nevidí váš profil. 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. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link inkognito přes odkaz na kontaktní adresu @@ -6897,6 +8779,10 @@ Servery SimpleX nevidí váš profil. pozvánka do skupiny %@ group name + + invite + No comment provided by engineer. + invited pozvánka @@ -6951,6 +8837,10 @@ Servery SimpleX nevidí váš profil. připojeno rcv group event chat item + + message + No comment provided by engineer. + message received zpráva přijata @@ -6976,6 +8866,10 @@ Servery SimpleX nevidí váš profil. moderovaný %@ marked deleted chat item preview text + + moderator + member role + months měsíců @@ -6984,7 +8878,7 @@ Servery SimpleX nevidí váš profil. never nikdy - No comment provided by engineer. + delete after time new message @@ -7015,8 +8909,8 @@ Servery SimpleX nevidí váš profil. off vypnuto enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7033,6 +8927,14 @@ Servery SimpleX nevidí váš profil. zapnuto group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner vlastník @@ -7047,6 +8949,14 @@ Servery SimpleX nevidí váš profil. peer-to-peer No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -7061,6 +8971,10 @@ Servery SimpleX nevidí váš profil. obdržel potvrzení… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call odmítnutý hovor @@ -7089,6 +9003,10 @@ Servery SimpleX nevidí váš profil. odstranil vás rcv group event chat item + + requested to connect + chat list item title + saved No comment provided by engineer. @@ -7097,6 +9015,10 @@ Servery SimpleX nevidí váš profil. saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec sek @@ -7122,6 +9044,12 @@ Servery SimpleX nevidí váš profil. odeslat přímou zprávu No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address profile update event chat item @@ -7158,10 +9086,18 @@ Servery SimpleX nevidí váš profil. neznámý connection info + + unknown servers + No comment provided by engineer. + unknown status No comment provided by engineer. + + unprotected + No comment provided by engineer. + updated group profile aktualizoval profil skupiny @@ -7200,6 +9136,10 @@ Servery SimpleX nevidí váš profil. přes relé No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) videohovoru (nešifrovaného e2e) @@ -7225,6 +9165,10 @@ Servery SimpleX nevidí váš profil. týdnů time unit + + when IP hidden + No comment provided by engineer. + yes ano @@ -7306,7 +9250,7 @@ Servery SimpleX nevidí váš profil.
- +
@@ -7342,7 +9286,7 @@ Servery SimpleX nevidí váš profil.
- +
@@ -7362,4 +9306,205 @@ Servery SimpleX nevidí váš profil.
+ +
+ +
+ + + %d new events + notification body + + + From %d chat(s) + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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 + 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. + No comment provided by engineer. + + +
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 29384aecc4..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 @@
- +
- - - - - - 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) @@ -127,9 +100,14 @@ %@ wurde erfolgreich überprüft No comment provided by engineer. + + %@ server + %@ Server + No comment provided by engineer. + %@ servers - %@-Server + %@ Server No comment provided by engineer. @@ -142,6 +120,11 @@ %@ will sich mit Ihnen verbinden! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ und %lld Mitglieder @@ -162,11 +145,36 @@ %d Tage time interval + + %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. + forward confirmation reason + %d hours %d Stunden time interval + + %d messages not forwarded + %d Nachrichten wurden nicht weitergeleitet + alert title + %d min %d min @@ -182,6 +190,11 @@ %d s time interval + + %d seconds(s) + %d Sekunde(n) + delete after time + %d skipped message(s) %d übersprungene Nachricht(en) @@ -252,11 +265,6 @@ %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 @@ -307,11 +315,6 @@ %u übersprungene Nachrichten. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (Neu) @@ -322,19 +325,9 @@ (Dieses Gerät hat v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen oder eine Verbindung über einen Link herzustellen, den Sie erhalten haben. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Neuen Kontakt hinzufügen**: Um einen Einmal-QR-Code oder -Link für Ihren Kontakt zu erzeugen. + + **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. @@ -342,13 +335,13 @@ **Gruppe erstellen**: Um eine neue Gruppe zu erstellen. 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. + + **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). + + **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. @@ -362,11 +355,16 @@ **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. + + **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 / 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. @@ -392,11 +390,6 @@ \*fett* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +426,6 @@ - Nachrichtenverlauf bearbeiten No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sek @@ -450,8 +438,9 @@ 1 day - täglich - time interval + Älter als ein Tag + delete after time +time interval 1 hour @@ -465,13 +454,30 @@ 1 month - monatlich - time interval + Älter als ein Monat + delete after time +time interval 1 week - wöchentlich - time interval + Älter als eine Woche + delete after time +time interval + + + 1 year + Älter als ein Jahr + delete after time + + + 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 @@ -488,11 +494,6 @@ 30 Sekunden No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -529,22 +530,17 @@ Abort - Abbrechen + Beenden No comment provided by engineer. Abort changing address - Wechsel der Empfängeradresse abbrechen + Wechsel der Empfängeradresse beenden No comment provided by engineer. Abort changing address? - Wechsel der Empfängeradresse abbrechen? - No comment provided by engineer. - - - About SimpleX - Über SimpleX + Wechsel der Empfängeradresse beenden? No comment provided by engineer. @@ -552,21 +548,27 @@ Über SimpleX Chat No comment provided by engineer. - - About SimpleX address - Über die SimpleX-Adresse + + About operators + Über die Betreiber No comment provided by engineer. - - Accent color - Akzentfarbe + + Accent + Akzent No comment provided by engineer. Accept Annehmen accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Nutzungsbedingungen akzeptieren + No comment provided by engineer. Accept connection request? @@ -581,21 +583,47 @@ Accept incognito Inkognito akzeptieren - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Akzeptierte Nutzungsbedingungen + No comment provided by engineer. + + + Acknowledged + Bestätigt + No comment provided by engineer. + + + Acknowledgement errors + Fehler bei der Bestätigung + No comment provided by engineer. + + + Active + Aktiv + token status text + + + 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 contact - Kontakt hinzufügen + + Add friends + Freunde aufnehmen No comment provided by engineer. - - Add preset servers - Füge voreingestellte Server hinzu + + Add list + Liste hinzufügen No comment provided by engineer. @@ -603,14 +631,19 @@ Profil hinzufügen No comment provided by engineer. - - Add servers by scanning QR codes. - Fügen Sie Server durch Scannen der QR Codes hinzu. + + Add server + Server hinzufügen No comment provided by engineer. - - Add server… - Füge Server 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. @@ -618,11 +651,46 @@ 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 @@ -630,7 +698,17 @@ Address change will be aborted. Old receiving address will be used. - Der Wechsel der Empfängeradresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet. + 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 No comment provided by engineer. @@ -648,6 +726,16 @@ Erweiterte Netzwerkeinstellungen No comment provided by engineer. + + Advanced settings + Erweiterte Einstellungen + No comment provided by engineer. + + + All + Alle + No comment provided by engineer. + All app data is deleted. Werden die App-Daten komplett gelöscht. @@ -655,27 +743,42 @@ 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! + Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. + + 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. + alert message + 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 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. Dieser Vorgang kann nicht rückgängig gemacht werden! + 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. - Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. + 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. @@ -683,6 +786,21 @@ Von %@ werden alle neuen Nachrichten ausgeblendet! No comment provided by engineer. + + All profiles + Alle Profile + profile dropdown + + + 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. @@ -695,7 +813,7 @@ 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-Server hochgeladen. + 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. @@ -708,11 +826,21 @@ Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt. No comment provided by engineer. + + Allow calls? + Anrufe erlauben? + No comment provided by engineer. + 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) @@ -738,11 +866,21 @@ Das Senden von verschwindenden Nachrichten erlauben. No comment provided by engineer. + + 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 SimpleX links. Das Senden von SimpleX-Links erlauben. @@ -808,6 +946,11 @@ Sie sind bereits Mitglied der Gruppe! 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 @@ -818,11 +961,21 @@ Es wurde ein leeres Chat-Profil mit dem eingegebenen Namen erstellt und die App öffnet wie gewohnt. No comment provided by engineer. + + Another reason + Anderer Grund + report reason + 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: %@ @@ -838,6 +991,11 @@ 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 @@ -853,6 +1011,11 @@ 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 @@ -873,11 +1036,56 @@ 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? + No comment provided by engineer. + + + Archive reports + Meldungen archivieren + swipe action + + + Archived contacts + Archivierte Kontakte + No comment provided by engineer. + Archiving database Datenbank wird archiviert @@ -943,11 +1151,21 @@ Bilder automatisch akzeptieren No comment provided by engineer. + + Auto-accept settings + Einstellungen automatisch akzeptieren + alert title + Back Zurück No comment provided by engineer. + + Background + Hintergrund-Farbe + No comment provided by engineer. + Bad desktop address Falsche Desktop-Adresse @@ -963,16 +1181,61 @@ Ungültiger Nachrichten-Hash 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 @@ -1008,6 +1271,16 @@ 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. + Both you and your contact can add message reactions. Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben. @@ -1038,11 +1311,35 @@ 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: +- send only legal content in public groups. +- respect other users – no spam. + Durch die Nutzung von SimpleX Chat erklären Sie sich damit einverstanden: +- nur legale Inhalte in öffentlichen Gruppen zu versenden. +- andere Nutzer zu respektieren - kein Spam. + No comment provided by engineer. + Call already ended! Anruf ist bereits beendet! @@ -1053,11 +1350,26 @@ Anrufe No comment provided by engineer. + + 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! @@ -1068,10 +1380,16 @@ Kontakte können nicht eingeladen werden! No comment provided by engineer. + + Can't message member + Mitglied kann nicht benachrichtigt werden + No comment provided by engineer. + Cancel Abbrechen - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,14 +1401,24 @@ 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 + No comment provided by engineer. + Cannot receive file Datei kann nicht empfangen werden - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen. + snd error text Cellular - Zellulär + Mobilfunknetz No comment provided by engineer. @@ -1098,6 +1426,16 @@ Ändern No comment provided by engineer. + + Change automatic message deletion? + Automatisches Löschen von Nachrichten ändern? + alert title + + + Change chat profiles + Chat-Profile wechseln + authentication reason + Change database passphrase? Datenbank-Passwort ändern? @@ -1142,11 +1480,26 @@ Change self-destruct passcode Selbstzerstörungs-Zugangscode ändern authentication reason - set passcode view +set passcode view - - 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. @@ -1164,6 +1517,11 @@ 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 @@ -1184,6 +1542,11 @@ 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! @@ -1194,15 +1557,50 @@ Chat-Präferenzen No comment provided by engineer. + + Chat preferences were changed. + Die Chat-Präferenzen wurden geändert. + alert message + + + 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. + No comment provided by engineer. + Check server address and try again. Überprüfen Sie die Serveradresse und versuchen Sie es nochmal. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,24 +1622,49 @@ Aus dem Fotoalbum auswählen No comment provided by engineer. + + Chunks deleted + Daten-Pakete gelöscht + No comment provided by engineer. + + + Chunks downloaded + Daten-Pakete heruntergeladen + No comment provided by engineer. + + + Chunks uploaded + Daten-Pakete hochgeladen + No comment provided by engineer. + Clear - Löschen - No comment provided by engineer. + Entfernen + swipe action Clear conversation - Chatinhalte löschen + Chat-Inhalte entfernen No comment provided by engineer. Clear conversation? - Unterhaltung löschen? + 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 löschen? + Private Notizen entfernen? No comment provided by engineer. @@ -1249,11 +1672,21 @@ Überprüfung zurücknehmen No comment provided by engineer. - - Colors - Farben + + Color chats with the new themes. + Farbige Chats mit neuen Designs. No comment provided by engineer. + + Color mode + Farbvariante + No comment provided by engineer. + + + Community guidelines violation + Verstoß gegen die Gemeinschaftsrichtlinien + report reason + Compare file Datei vergleichen @@ -1264,11 +1697,56 @@ Vergleichen Sie die Sicherheitscodes mit Ihren Kontakten. 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 @@ -1279,11 +1757,21 @@ Zugangscode 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 @@ -1309,6 +1797,11 @@ Hochladen bestätigen No comment provided by engineer. + + Confirmed + Bestätigt + token status text + Connect Verbinden @@ -1329,6 +1822,11 @@ Mit dem Desktop 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? @@ -1368,16 +1866,31 @@ Das ist Ihr eigener Einmal-Link! Mit %@ verbinden No comment provided by engineer. + + Connected + Verbunden + No comment provided by engineer. + Connected desktop Verbundener Desktop 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 + Verbinden + No comment provided by engineer. + Connecting to server… Mit dem Server verbinden… @@ -1388,6 +1901,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -1398,6 +1916,16 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -1408,11 +1936,38 @@ Das ist Ihr eigener Einmal-Link! Verbindungsfehler (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + Die Verbindung wurde vom Server-Betreiber blockiert: +%@ + 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 @@ -1423,6 +1978,16 @@ Das ist Ihr eigener Einmal-Link! Verbindungszeitüberschreitung No comment provided by engineer. + + Connection with desktop stopped + Die Verbindung mit dem Desktop wurde gestoppt + No comment provided by engineer. + + + Connections + Verbindungen + No comment provided by engineer. + Contact allows Der Kontakt erlaubt @@ -1433,6 +1998,11 @@ Das ist Ihr eigener Einmal-Link! Der Kontakt ist bereits vorhanden No comment provided by engineer. + + Contact deleted! + Kontakt gelöscht! + No comment provided by engineer. + Contact hidden: Kontakt verborgen: @@ -1443,9 +2013,9 @@ Das ist Ihr eigener Einmal-Link! Mit Ihrem Kontakt verbunden notification - - Contact is not connected yet! - Ihr Kontakt ist noch nicht verbunden! + + Contact is deleted. + Kontakt wurde gelöscht. No comment provided by engineer. @@ -1458,6 +2028,11 @@ Das ist Ihr eigener Einmal-Link! 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 @@ -1468,21 +2043,41 @@ Das ist Ihr eigener Einmal-Link! Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen. No comment provided by engineer. + + Content violates conditions of use + Inhalt verletzt Nutzungsbedingungen + blocking reason + Continue Weiter No comment provided by engineer. + + Conversation deleted! + Chat-Inhalte entfernt! + No comment provided by engineer. + Copy Kopieren - chat item action + 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 %@? @@ -1493,6 +2088,11 @@ Das ist Ihr eigener Einmal-Link! Erstellen No comment provided by engineer. + + Create 1-time link + Einmal-Link erstellen + No comment provided by engineer. + Create SimpleX address SimpleX-Adresse erstellen @@ -1503,11 +2103,6 @@ Das ist Ihr eigener Einmal-Link! Erstellen Sie eine Gruppe mit einem zufälligen Profil. 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. - No comment provided by engineer. - Create file Datei erstellen @@ -1528,6 +2123,11 @@ Das ist Ihr eigener Einmal-Link! Link erzeugen No comment provided by engineer. + + Create list + Liste erstellen + 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/). 💻 @@ -1553,6 +2153,11 @@ Das ist Ihr eigener Einmal-Link! Erstellen Sie Ihr Profil No comment provided by engineer. + + Created + Erstellt + No comment provided by engineer. + Created at Erstellt um @@ -1563,11 +2168,6 @@ Das ist Ihr eigener Einmal-Link! Erstellt um: %@ copied message info - - Created on %@ - Erstellt am %@ - No comment provided by engineer. - Creating archive link Archiv-Link erzeugen @@ -1583,11 +2183,21 @@ Das ist Ihr eigener Einmal-Link! Aktueller Zugangscode 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 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 %@. @@ -1598,11 +2208,26 @@ Das ist Ihr eigener Einmal-Link! 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 ID Datenbank-ID @@ -1701,6 +2326,11 @@ Das ist Ihr eigener Einmal-Link! Die Datenbank wird beim nächsten Start der App migriert No comment provided by engineer. + + Debug delivery + Debugging-Zustellung + No comment provided by engineer. + Decentralized Dezentral @@ -1714,18 +2344,19 @@ Das ist Ihr eigener Einmal-Link! Delete Löschen - chat item action + alert action +swipe action + + + Delete %lld messages of members? + %lld Nachrichten der Mitglieder löschen? + No comment provided by engineer. Delete %lld messages? %lld Nachrichten löschen? No comment provided by engineer. - - Delete Contact - Kontakt löschen - No comment provided by engineer. - Delete address Adresse löschen @@ -1751,14 +2382,14 @@ Das ist Ihr eigener Einmal-Link! 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. @@ -1771,6 +2402,11 @@ Das ist Ihr eigener Einmal-Link! Chat-Profil löschen? No comment provided by engineer. + + Delete chat? + Chat löschen? + No comment provided by engineer. + Delete connection Verbindung löschen @@ -1781,11 +2417,9 @@ Das ist Ihr eigener Einmal-Link! Kontakt löschen No comment provided by engineer. - - Delete contact? -This cannot be undone! - Kontakt löschen? -Das kann nicht rückgängig gemacht werden! + + Delete contact? + Kontakt löschen? No comment provided by engineer. @@ -1848,6 +2482,11 @@ Das kann nicht rückgängig gemacht werden! Link löschen? No comment provided by engineer. + + Delete list? + Liste löschen? + alert title + Delete member message? Nachricht des Mitglieds löschen? @@ -1861,11 +2500,11 @@ Das kann nicht rückgängig gemacht werden! Delete messages Nachrichten löschen - No comment provided by engineer. + alert button Delete messages after - Löschen der Nachrichten + Nachrichten löschen No comment provided by engineer. @@ -1878,14 +2517,14 @@ Das kann nicht rückgängig gemacht werden! 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? + Ausstehende Verbindung löschen? No comment provided by engineer. @@ -1898,11 +2537,31 @@ Das kann nicht rückgängig gemacht werden! Lösche Warteschlange server test step + + 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 + No comment provided by engineer. + + + Deleted + Gelöscht + No comment provided by engineer. + Deleted at Gelöscht um @@ -1913,6 +2572,16 @@ Das kann nicht rückgängig gemacht werden! Gelöscht um: %@ copied message info + + 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 @@ -1948,11 +2617,41 @@ Das kann nicht rückgängig gemacht werden! 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 %@. + No comment provided by engineer. + + + Destination server error: %@ + Zielserver-Fehler: %@ + snd error text + + + 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 @@ -1983,8 +2682,13 @@ Das kann nicht rückgängig gemacht werden! Direkte Nachrichten chat feature - - Direct messages between members are prohibited in this group. + + 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. @@ -1998,11 +2702,26 @@ Das kann nicht rückgängig gemacht werden! SimpleX-Sperre deaktivieren authentication reason + + Disable automatic message deletion? + Automatisches Löschen von Nachrichten deaktivieren? + alert title + + + Disable delete messages + Löschen von Nachrichten deaktivieren + alert button + Disable for all Für Alle deaktivieren No comment provided by engineer. + + Disabled + Deaktiviert + No comment provided by engineer. + Disappearing message Verschwindende Nachricht @@ -2018,8 +2737,8 @@ Das kann nicht rückgängig gemacht werden! In diesem Chat sind verschwindende Nachrichten nicht erlaubt. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt. No comment provided by engineer. @@ -2053,9 +2772,19 @@ Das kann nicht rückgängig gemacht werden! Lokales Netzwerk durchsuchen 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 SimpleX for emergency calls. - Nutzen Sie SimpleX nicht für Notrufe. + SimpleX NICHT für Notrufe nutzen. + No comment provided by engineer. + + + Do NOT use private routing. + Sie nutzen KEIN privates Routing. No comment provided by engineer. @@ -2068,6 +2797,16 @@ Das kann nicht rückgängig gemacht werden! Den Nachrichtenverlauf nicht an neue Mitglieder senden. No comment provided by engineer. + + Do not use credentials with proxy. + Verwenden Sie keine Anmeldeinformationen mit einem Proxy. + No comment provided by engineer. + + + Documents: + Dokumente: + No comment provided by engineer. + Don't create address Keine Adresse erstellt @@ -2078,11 +2817,21 @@ Das kann nicht rückgängig gemacht werden! 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 @@ -2091,7 +2840,13 @@ Das kann nicht rückgängig gemacht werden! Download Herunterladen - chat item action + alert button +chat item action + + + Download errors + Fehler beim Herunterladen + No comment provided by engineer. Download failed @@ -2103,6 +2858,21 @@ Das kann nicht rückgängig gemacht werden! Datei herunterladen server test step + + Download files + Dateien herunterladen + alert action + + + Downloaded + Heruntergeladen + No comment provided by engineer. + + + Downloaded files + Heruntergeladene Dateien + No comment provided by engineer. + Downloading archive Archiv wird heruntergeladen @@ -2123,6 +2893,11 @@ Das kann nicht rückgängig gemacht werden! Dauer No comment provided by engineer. + + E2E encrypted notifications. + E2E-verschlüsselte Benachrichtigungen. + No comment provided by engineer. + Edit Bearbeiten @@ -2143,6 +2918,11 @@ Das kann nicht rückgängig gemacht werden! Aktivieren (vorgenommene Einstellungen bleiben erhalten) 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 SimpleX Lock SimpleX-Sperre aktivieren @@ -2156,7 +2936,7 @@ Das kann nicht rückgängig gemacht werden! Enable automatic message deletion? Automatisches Löschen von Nachrichten aktivieren? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2983,11 @@ Das kann nicht rückgängig gemacht werden! Selbstzerstörungs-Zugangscode aktivieren set passcode view + + Enabled + Aktiviert + No comment provided by engineer. + Enabled for Aktiviert für @@ -2278,6 +3063,11 @@ Das kann nicht rückgängig gemacht werden! Neuverhandlung der Verschlüsselung fehlgeschlagen. No comment provided by engineer. + + Encryption renegotiation in progress. + Die Neuverhandlung der Verschlüsselung läuft. + No comment provided by engineer. + Enter Passcode Zugangscode eingeben @@ -2340,29 +3130,39 @@ Das kann nicht rückgängig gemacht werden! Error aborting address change - Fehler beim Abbrechen des Adresswechsels + Fehler beim Beenden des Adresswechsels No comment provided by engineer. + + Error accepting conditions + Fehler beim Akzeptieren der Nutzungsbedingungen + alert title + 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 No comment provided by engineer. + + Error adding server + Fehler beim Hinzufügen des Servers + alert title + 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 @@ -2373,6 +3173,21 @@ Das kann nicht rückgängig gemacht werden! 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 @@ -2388,6 +3203,11 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Erzeugen des Gruppen-Links No comment provided by engineer. + + Error creating list + Fehler beim Erstellen der Liste + alert title + Error creating member contact Fehler beim Anlegen eines Mitglied-Kontaktes @@ -2403,6 +3223,11 @@ Das kann nicht rückgängig gemacht werden! 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 @@ -2423,11 +3248,6 @@ Das kann nicht rückgängig gemacht werden! 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 @@ -2473,6 +3293,11 @@ Das kann nicht rückgängig gemacht werden! 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 @@ -2483,9 +3308,14 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Beitritt zur Gruppe No comment provided by engineer. - - Error loading %@ servers - Fehler beim Laden von %@ Servern + + Error loading servers + Fehler beim Laden der Server + alert title + + + Error migrating settings + Fehler beim Migrieren der Einstellungen No comment provided by engineer. @@ -2495,17 +3325,37 @@ Das kann nicht rückgängig gemacht werden! Error receiving file - Fehler beim Empfangen der Datei + Fehler beim Herunterladen der Datei + alert title + + + 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 + No comment provided by engineer. + + + Error registering for notifications + Fehler beim Registrieren für Benachrichtigungen + alert title + Error removing member Fehler beim Entfernen des Mitglieds No comment provided by engineer. - - Error saving %@ servers - Fehler beim Speichern der %@-Server + + Error reordering lists + Fehler beim Umsortieren der Listen + alert title + + + Error resetting statistics + Fehler beim Zurücksetzen der Statistiken No comment provided by engineer. @@ -2513,6 +3363,11 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Speichern der ICE-Server No comment provided by engineer. + + Error saving chat list + Fehler beim Speichern der Chat-Liste + alert title + Error saving group profile Fehler beim Speichern des Gruppenprofils @@ -2528,6 +3383,11 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Speichern des Passworts in den Schlüsselbund No comment provided by engineer. + + Error saving servers + Fehler beim Speichern der Server + alert title + Error saving settings Fehler beim Abspeichern der Einstellungen @@ -2573,16 +3433,26 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Beenden des Chats No comment provided by engineer. + + Error switching profile + Fehler beim Wechseln des Profils + No comment provided by engineer. + Error switching profile! Fehler beim Umschalten des Profils! - No comment provided by engineer. + alertTitle 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 @@ -2593,6 +3463,11 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Aktualisieren der Nachricht No comment provided by engineer. + + Error updating server + Fehler beim Aktualisieren des Servers + alert title + Error updating settings Fehler beim Aktualisieren der Einstellungen @@ -2621,7 +3496,9 @@ Das kann nicht rückgängig gemacht werden! Error: %@ Fehler: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3510,16 @@ Das kann nicht rückgängig gemacht werden! Fehler: Keine Datenbankdatei No comment provided by engineer. + + Errors + Fehler + No comment provided by engineer. + + + Errors in servers configuration. + Fehler in der Server-Konfiguration. + servers error + Even when disabled in the conversation. Auch wenn sie im Chat deaktiviert sind. @@ -2648,6 +3535,11 @@ Das kann nicht rückgängig gemacht werden! Erweitern chat item action + + Expired + Abgelaufen + token status text + Export database Datenbank exportieren @@ -2658,6 +3550,11 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Export: No comment provided by engineer. + + Export theme + Design exportieren + No comment provided by engineer. + Exported database archive. Exportiertes Datenbankarchiv. @@ -2683,16 +3580,70 @@ Das kann nicht rückgängig gemacht werden! 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. + No comment provided by engineer. + Favorite Favorit + swipe action + + + Favorites + Favoriten No comment provided by engineer. + + File error + Datei-Fehler + file error alert title + + + File errors: +%@ + Datei-Fehler: +%@ + alert message + + + File is blocked by server operator: +%@. + Datei wurde vom Server-Betreiber blockiert: +%@. + 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: %@ + file error text + + + File status + Datei-Status + No comment provided by engineer. + + + File status: %@ + Datei-Status: %@ + copied message info + File will be deleted from servers. Die Datei wird von den Servern gelöscht. @@ -2700,12 +3651,12 @@ Das kann nicht rückgängig gemacht werden! File will be received when your contact completes uploading it. - Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist. + 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! + Die Datei wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach! No comment provided by engineer. @@ -2713,6 +3664,11 @@ Das kann nicht rückgängig gemacht werden! Datei: %@ No comment provided by engineer. + + Files + Dateien + No comment provided by engineer. + Files & media Dateien & Medien @@ -2723,8 +3679,8 @@ Das kann nicht rückgängig gemacht werden! Dateien und Medien chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. In dieser Gruppe sind Dateien und Medien nicht erlaubt. No comment provided by engineer. @@ -2793,21 +3749,71 @@ Das kann nicht rückgängig gemacht werden! Reparatur wird vom Gruppenmitglied nicht unterstützt No comment provided by engineer. + + For all moderators + Für alle Moderatoren + No comment provided by engineer. + + + For chat profile %@: + Für das Chat-Profil %@: + servers error + 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 + No comment provided by engineer. + Forward Weiterleiten chat item action + + Forward %d message(s)? + %d Nachricht(en) weiterleiten? + alert title + Forward and save messages Nachrichten weiterleiten und speichern No comment provided by engineer. + + Forward messages + Nachrichten weiterleiten + alert action + + + Forward messages without files? + Nachrichten ohne Dateien weiterleiten? + alert message + + + Forward up to 20 messages at once. + Bis zu 20 Nachrichten auf einmal weiterleiten + No comment provided by engineer. + Forwarded Weitergeleitet @@ -2818,6 +3824,40 @@ Das kann nicht rückgängig gemacht werden! 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Weiterleitungsserver: %1$@ +Zielserver Fehler: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Weiterleitungsserver: %1$@ +Fehler: %2$@ + snd error text + Found desktop Gefundener Desktop @@ -2838,11 +3878,6 @@ Das kann nicht rückgängig gemacht werden! 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. @@ -2863,6 +3898,21 @@ Das kann nicht rückgängig gemacht werden! GIFs und Sticker No comment provided by engineer. + + Get notified when mentioned. + Bei Erwähnung benachrichtigt werden. + No comment provided by engineer. + + + Good afternoon! + Guten Nachmittag! + message preview + + + Good morning! + Guten Morgen! + message preview + Group Gruppe @@ -2918,41 +3968,6 @@ Das kann nicht rückgängig gemacht werden! 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. (24 hours) - Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden) - No comment provided by engineer. - - - Group members can send SimpleX links. - Gruppenmitglieder können SimpleX-Links senden. - 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. - No comment provided by engineer. - Group message: Grppennachricht: @@ -2985,12 +4000,17 @@ Das 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! + 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! + 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. @@ -2998,6 +4018,11 @@ Das kann nicht rückgängig gemacht werden! 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 @@ -3048,10 +4073,20 @@ Das kann nicht rückgängig gemacht werden! Wie SimpleX funktioniert 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 + No comment provided by engineer. + How it works Wie es funktioniert - No comment provided by engineer. + alert button How to @@ -3078,6 +4113,11 @@ Das kann nicht rückgängig gemacht werden! ICE-Server (einer pro Zeile) No comment provided by engineer. + + IP address + IP-Adresse + 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. @@ -3105,12 +4145,12 @@ Das kann nicht rückgängig gemacht werden! Image will be received when your contact completes uploading it. - Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist. + 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! + Das Bild wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach! No comment provided by engineer. @@ -3118,8 +4158,8 @@ Das kann nicht rückgängig gemacht werden! Sofort No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Immun gegen Spam und Missbrauch No comment provided by engineer. @@ -3143,11 +4183,23 @@ Das kann nicht rückgängig gemacht werden! 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. +More improvements are coming soon! + Verbesserte Nachrichten-Auslieferung und verringerter Datenverbrauch. +Weitere Verbesserungen sind bald verfügbar! + No comment provided by engineer. + Improved message delivery Verbesserte Zustellung von Nachrichten @@ -3178,6 +4230,16 @@ Das kann nicht rückgängig gemacht werden! Klingeltöne No comment provided by engineer. + + Inappropriate content + Unangemessener Inhalt + report reason + + + Inappropriate profile + Unangemessenes Profil + report reason + Incognito Inkognito @@ -3215,12 +4277,12 @@ Das kann nicht rückgängig gemacht werden! Incompatible database version - Inkompatible Datenbank-Version + Datenbank-Version nicht kompatibel No comment provided by engineer. Incompatible version - Inkompatible Version + Version nicht kompatibel No comment provided by engineer. @@ -3248,6 +4310,11 @@ Das kann nicht rückgängig gemacht werden! 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! @@ -3255,16 +4322,41 @@ Das kann nicht rückgängig gemacht werden! No comment provided by engineer. - - Instantly - Sofort - No comment provided by engineer. - Interface Schnittstelle No comment provided by engineer. + + Interface colors + Interface-Farben + No comment provided by engineer. + + + 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) + token status text + Invalid QR code Ungültiger QR-Code @@ -3303,7 +4395,7 @@ Das kann nicht rückgängig gemacht werden! Invalid server address! Ungültige Serveradresse! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4417,11 @@ Das kann nicht rückgängig gemacht werden! Mitglieder einladen No comment provided by engineer. + + Invite to chat + Zum Chat einladen + No comment provided by engineer. + Invite to group In Gruppe einladen @@ -3340,8 +4437,8 @@ Das kann nicht rückgängig gemacht werden! 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. + + Irreversible message deletion is prohibited. In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt. No comment provided by engineer. @@ -3366,6 +4463,11 @@ Das kann nicht rückgängig gemacht werden! 3. 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 (%@). @@ -3384,7 +4486,7 @@ Das kann nicht rückgängig gemacht werden! Join Beitreten - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4528,11 @@ Das ist Ihr Link für die Gruppe %@! Keep Behalten + alert action + + + Keep conversation + Chat-Inhalte beibehalten No comment provided by engineer. @@ -3436,7 +4543,7 @@ Das ist Ihr Link für die Gruppe %@! Keep unused invitation? Nicht genutzte Einladung behalten? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4578,16 @@ Das ist Ihr Link für die Gruppe %@! Leave Verlassen + swipe action + + + Leave chat + Chat verlassen + No comment provided by engineer. + + + Leave chat? + Chat verlassen? No comment provided by engineer. @@ -3513,6 +4630,21 @@ Das ist Ihr Link für die Gruppe %@! Verknüpfte Desktops No comment provided by engineer. + + List + Liste + swipe action + + + 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 message! Live Nachricht! @@ -3523,11 +4655,6 @@ Das ist Ihr Link für die Gruppe %@! Live Nachrichten No comment provided by engineer. - - Local - Lokal - No comment provided by engineer. - Local name Lokaler Name @@ -3548,11 +4675,6 @@ Das ist Ihr Link für die Gruppe %@! 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 @@ -3563,21 +4685,11 @@ Das ist Ihr Link für die Gruppe %@! 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 getrennt 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?* - No comment provided by engineer. - Mark deleted for everyone Für Alle als gelöscht markieren @@ -3603,11 +4715,36 @@ Das ist Ihr Link für die Gruppe %@! Max. 30 Sekunden, sofort erhalten. No comment provided by engineer. + + Media & file servers + Medien- und Datei-Server + No comment provided by engineer. + + + Medium + Medium + blur media + Member Mitglied No comment provided by engineer. + + Member inactive + Mitglied inaktiv + item status text + + + Member reports + Mitglieder-Meldungen + chat feature + + + 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. @@ -3618,9 +4755,64 @@ Das ist Ihr Link für die Gruppe %@! Die Mitgliederrolle wird auf "%@" geändert. Das Mitglied wird eine neue Einladung erhalten. No comment provided by engineer. + + 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! + 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 SimpleX links. + Gruppenmitglieder können SimpleX-Links versenden. + 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 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. @@ -3633,11 +4825,31 @@ Das ist Ihr Link für die Gruppe %@! Empfangsbestätigungen für Nachrichten! No comment provided by engineer. + + Message delivery warning + Warnung bei der Nachrichtenzustellung + item status text + Message draft Nachrichtenentwurf No comment provided by engineer. + + Message forwarded + Nachricht weitergeleitet + item status text + + + Message may be delivered later if member becomes active. + Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird. + item status description + + + Message queue info + Nachrichten-Warteschlangen-Information + No comment provided by engineer. + Message reactions Reaktionen auf Nachrichten @@ -3648,16 +4860,41 @@ Das ist Ihr Link für die Gruppe %@! In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt. 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 + No comment provided by engineer. + + + Message status: %@ + Nachrichten-Status: %@ + copied message info + Message text Nachrichtentext @@ -3683,14 +4920,34 @@ Das ist Ihr Link für die Gruppe %@! Die Nachrichten von %@ werden angezeigt! No comment provided by engineer. + + Messages in this chat will never be deleted. + Nachrichten in diesem Chat werden nie gelöscht. + alert message + + + Messages received + Empfangene Nachrichten + No comment provided by engineer. + + + Messages sent + Gesendete Nachrichten + No comment provided by engineer. + + + Messages were deleted after you selected them. + Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten. + alert message + 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, Ablehnung und Einbruchs-Wiederherstellung geschützt. + 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, Ablehnung und Einbruchs-Wiederherstellung geschützt. + 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. @@ -3748,9 +5005,9 @@ Das ist Ihr Link für die Gruppe %@! Die Migration wurde abgeschlossen No comment provided by engineer. - - Migrations: %@ - Migrationen: %@ + + Migrations: + Migrationen: No comment provided by engineer. @@ -3768,6 +5025,11 @@ Das ist Ihr Link für die Gruppe %@! Moderiert um: %@ copied message info + + More + Mehr + swipe action + More improvements are coming soon! Weitere Verbesserungen sind bald verfügbar! @@ -3778,16 +5040,16 @@ Das ist Ihr Link für die Gruppe %@! Zuverlässigere Netzwerkverbindung. No comment provided by engineer. + + More reliable notifications + Zuverlässigere Benachrichtigungen + No comment provided by engineer. + Most likely this connection is deleted. Wahrscheinlich ist diese Verbindung gelöscht worden. item status description - - 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 @@ -3796,7 +5058,12 @@ Das ist Ihr Link für die Gruppe %@! Mute Stummschalten - No comment provided by engineer. + notification label action + + + Mute all + Alle stummschalten + notification label action Muted when inactive! @@ -3806,7 +5073,7 @@ Das ist Ihr Link für die Gruppe %@! Name Name - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +5085,26 @@ Das ist Ihr Link für die Gruppe %@! Netzwerkverbindung No comment provided by engineer. + + Network decentralization + Dezentralisiertes Netzwerk + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen. + snd error text + Network management Netzwerk-Verwaltung No comment provided by engineer. + + Network operator + Netzwerk-Betreiber + No comment provided by engineer. + Network settings Netzwerkeinstellungen @@ -3833,16 +5115,36 @@ Das ist Ihr Link für die Gruppe %@! Netzwerkstatus No comment provided by engineer. + + New + Neu + token status text + New Passcode Neuer Zugangscode 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 + No comment provided by engineer. + New chat Neuer Chat No comment provided by engineer. + + New chat experience 🎉 + Neue Chat-Erfahrung 🎉 + No comment provided by engineer. + New contact request Neue Kontaktanfrage @@ -3853,11 +5155,6 @@ Das ist Ihr Link für die Gruppe %@! Neuer Kontakt: notification - - New database archive - Neues Datenbankarchiv - No comment provided by engineer. - New desktop app! Neue Desktop-App! @@ -3868,11 +5165,21 @@ Das ist Ihr Link für die Gruppe %@! Neuer Anzeigename No comment provided by engineer. + + New events + Neue Ereignisse + notification + 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 @@ -3888,6 +5195,11 @@ Das ist Ihr Link für die Gruppe %@! Neues Passwort… No comment provided by engineer. + + New server + Neuer Server + No comment provided by engineer. + No Nein @@ -3898,6 +5210,21 @@ Das ist Ihr Link für die Gruppe %@! Kein App-Passwort Authentication unavailable + + 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 @@ -3918,6 +5245,11 @@ Das ist Ihr Link für die Gruppe %@! Kein Geräte-Token! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet. + item status description + No filtered chats Keine gefilterten Chats @@ -3933,19 +5265,89 @@ Das ist Ihr Link für die Gruppe %@! Kein Nachrichtenverlauf No comment provided by engineer. + + No info, try to reload + Keine Information - es wird versucht neu zu laden + No comment provided by engineer. + + + No media & file servers. + Keine Medien- und Dateiserver. + servers error + + + No message + Keine Nachricht + No comment provided by engineer. + + + No message servers. + Keine Nachrichten-Server. + servers error + 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 push server + Lokal + No comment provided by engineer. + No received or sent files - Keine empfangenen oder gesendeten Dateien + Keine herunter- oder hochgeladenen Dateien + No comment provided by engineer. + + + 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. + servers error + + + No token! + Kein Token! + alert title + + + No unread chats + Keine ungelesenen Chats + No comment provided by engineer. + + + No user identifiers. + Keine Benutzerkennungen. No comment provided by engineer. @@ -3953,6 +5355,21 @@ Das ist Ihr Link für die Gruppe %@! Nicht kompatibel! No comment provided by engineer. + + Notes + Anmerkungen + No comment provided by engineer. + + + Nothing selected + Nichts ausgewählt + No comment provided by engineer. + + + Nothing to forward! + Es gibt nichts zum Weiterleiten! + alert title + Notifications Benachrichtigungen @@ -3963,6 +5380,21 @@ Das ist Ihr Link für die Gruppe %@! Benachrichtigungen sind deaktiviert! No comment provided by engineer. + + Notifications error + Benachrichtigungs-Fehler + alert title + + + Notifications privacy + Datenschutz für Benachrichtigungen + No comment provided by engineer. + + + Notifications status + Benachrichtigungs-Status + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5412,35 @@ Das ist Ihr Link für die Gruppe %@! Off Aus - No comment provided by engineer. + blur media Ok Ok - No comment provided by engineer. + alert button Old database Alte Datenbank No comment provided by engineer. - - Old database archive - Altes Datenbankarchiv - No comment provided by engineer. - 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. +Requires compatible VPN. + Für diese Verbindung werden Onion-Hosts benötigt. +Dies 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. +Requires compatible VPN. + Wenn Onion-Hosts verfügbar sind, werden sie verwendet. +Dies erfordert die Aktivierung eines VPNs. No comment provided by engineer. @@ -4017,11 +5448,21 @@ Das ist Ihr Link für die Gruppe %@! 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**. + + 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. @@ -4037,6 +5478,16 @@ Das ist Ihr Link für die Gruppe %@! 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. @@ -4090,13 +5541,18 @@ Das ist Ihr Link für die Gruppe %@! Open Öffnen - No comment provided by engineer. + alert action Open Settings Geräte-Einstellungen öffnen No comment provided by engineer. + + Open changes + Änderungen öffnen + No comment provided by engineer. + Open chat Chat öffnen @@ -4107,31 +5563,45 @@ Das ist Ihr Link für die Gruppe %@! Chat-Konsole öffnen authentication reason + + Open conditions + Nutzungsbedingungen öffnen + No comment provided by engineer. + Open group Gruppe öffnen No comment provided by engineer. + + Open link? + alert title + Open migration to another device Migration auf ein anderes Gerät öffnen authentication reason - - Open user profiles - Benutzerprofile öffnen - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen. - No comment provided by engineer. - Opening app… App wird geöffnet… No comment provided by engineer. + + Operator + Betreiber + No comment provided by engineer. + + + Operator server + Betreiber-Server + alert title + + + 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 @@ -4152,11 +5622,28 @@ Das ist Ihr Link für die Gruppe %@! 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 file errors: +%@ + Andere(r) Datei-Fehler: +%@ + alert message + PING count PING-Zähler @@ -4192,6 +5679,11 @@ Das ist Ihr Link für die Gruppe %@! Zugangscode eingestellt! No comment provided by engineer. + + Password + Passwort + No comment provided by engineer. + Password to show Passwort anzeigen @@ -4222,13 +5714,13 @@ Das ist Ihr Link für die Gruppe %@! Fügen Sie den erhaltenen Link ein 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 + + Periodic Periodisch No comment provided by engineer. @@ -4242,11 +5734,28 @@ Das ist Ihr Link für die Gruppe %@! Bild-in-Bild-Anrufe 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. +Please 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. +Bitte 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. @@ -4314,11 +5823,31 @@ Fehler: %@ Bitte bewahren Sie das Passwort sicher auf, Sie können es NICHT mehr ändern, wenn Sie es vergessen haben oder verlieren. No comment provided by engineer. + + 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. + token info + Polish interface Polnische Bedienoberfläche No comment provided by engineer. + + Port + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig @@ -4329,41 +5858,86 @@ Fehler: %@ Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren. No comment provided by engineer. - - Preset server - Voreingestellter Server - No comment provided by engineer. - Preset server address Voreingestellte Serveradresse No comment provided by engineer. + + 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 🚀 + No comment provided by engineer. + Private notes Private Notizen name of notes to self + + 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 @@ -4379,25 +5953,20 @@ Fehler: %@ Profil-Bilder No comment provided by engineer. - - Profile name - Profilname - No comment provided by engineer. - - - Profile name: - Profilname: - No comment provided by engineer. - Profile password Passwort für Profil No comment provided by engineer. + + Profile theme + Profil-Design + No comment provided by engineer. + Profile update will be sent to your contacts. Profil-Aktualisierung wird an Ihre Kontakte gesendet. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,6 +5988,11 @@ Fehler: %@ 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 SimpleX links. Das Senden von SimpleX-Links nicht erlauben. @@ -4444,11 +6018,23 @@ Fehler: %@ Das Senden von Sprachnachrichten nicht erlauben. No comment provided by engineer. + + Protect IP address + IP-Adresse schützen + No comment provided by engineer. + Protect app screen App-Bildschirm schützen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais, die Ihre Kontakte ausgewählt haben. +Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. + No comment provided by engineer. + Protect your chat profiles with a password! Ihre Chat-Profile mit einem Passwort schützen! @@ -4464,6 +6050,21 @@ Fehler: %@ 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 @@ -4484,6 +6085,11 @@ Fehler: %@ Bewerten Sie die App No comment provided by engineer. + + Reachable chat toolbar + Chat-Symbolleiste unten + No comment provided by engineer. + React… Reagiere… @@ -4492,33 +6098,28 @@ Fehler: %@ Read Gelesen - No comment provided by engineer. + swipe action 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. - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) lesen. + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/readme.html#connect-to-friends) lesen. No comment provided by engineer. - - Read more in our GitHub repository. - Erfahren Sie in unserem GitHub-Repository mehr dazu. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Erfahren Sie in unserem [GitHub-Repository](https://github.com/simplex-chat/simplex-chat#readme) mehr dazu. @@ -4529,6 +6130,11 @@ Fehler: %@ Bestätigungen sind deaktiviert No comment provided by engineer. + + Receive errors + Fehler beim Empfang + No comment provided by engineer. + Received at Empfangen um @@ -4549,19 +6155,29 @@ Fehler: %@ Empfangene Nachricht message info title + + 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 concurrency - Gleichzeitiger Empfang - No comment provided by engineer. - Receiving file will be stopped. - Der Empfang der Datei wird beendet. + Das Herunterladen der Datei wird beendet. No comment provided by engineer. @@ -4584,11 +6200,36 @@ Fehler: %@ 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? @@ -4609,10 +6250,26 @@ Fehler: %@ Reduzierter Batterieverbrauch No comment provided by engineer. + + Register + Registrieren + No comment provided by engineer. + + + Register notification token? + Benachrichtigungs-Token registrieren? + token info + + + Registered + Registriert + token status text + Reject Ablehnen - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4639,6 +6296,16 @@ Fehler: %@ 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 @@ -4699,6 +6366,56 @@ Fehler: %@ Antwort chat item action + + Report + Melden + chat item action + + + 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. + report reason + + + Report reason? + Grund der Meldung? + No comment provided by engineer. + + + 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 reason + + + Report: %@ + Meldung: %@ + report in notification + + + Reporting messages to moderators is prohibited. + Melden von Nachrichten an Moderatoren ist nicht erlaubt. + No comment provided by engineer. + + + Reports + Meldungen + No comment provided by engineer. + Required Erforderlich @@ -4709,16 +6426,41 @@ Fehler: %@ 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 @@ -4759,9 +6501,9 @@ Fehler: %@ Aufdecken chat item action - - Revert - Zurückkehren + + Review conditions + Nutzungsbedingungen einsehen No comment provided by engineer. @@ -4789,11 +6531,21 @@ Fehler: %@ Chat starten No comment provided by engineer. - - SMP servers + + SMP server SMP-Server No comment provided by engineer. + + SOCKS proxy + SOCKS-Proxy + No comment provided by engineer. + + + Safely receive files + Dateien sicher herunterladen + No comment provided by engineer. + Safer groups Sicherere Gruppen @@ -4802,43 +6554,44 @@ Fehler: %@ Save Speichern - chat item action + alert button +chat item action 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. + alert button 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 @@ -4852,7 +6605,7 @@ Fehler: %@ Save preferences? Präferenzen speichern? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6620,18 @@ Fehler: %@ Save servers? Alle Server speichern? - No comment provided by engineer. - - - Save settings? - Einstellungen speichern? - No comment provided by engineer. + alert title Save welcome message? Begrüßungsmeldung speichern? No comment provided by engineer. + + Save your profile? + Ihr Profil speichern? + alert title + Saved Abgespeichert @@ -4899,6 +6652,21 @@ Fehler: %@ Gespeicherte Nachricht message info title + + 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 QR code QR-Code scannen @@ -4936,7 +6704,12 @@ Fehler: %@ Search or paste SimpleX link - Suchen oder fügen Sie den SimpleX-Link ein + Suchen oder SimpleX-Link einfügen + No comment provided by engineer. + + + Secondary + Zweite Farbe No comment provided by engineer. @@ -4944,6 +6717,11 @@ Fehler: %@ Sichere Warteschlange server test step + + Secured + Abgesichert + No comment provided by engineer. + Security assessment Sicherheits-Gutachten @@ -4957,6 +6735,21 @@ Fehler: %@ Select Auswählen + chat item action + + + 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. @@ -4994,11 +6787,6 @@ Fehler: %@ Empfangsbestätigungen senden an 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 @@ -5009,6 +6797,11 @@ Fehler: %@ Verschwindende Nachricht senden No comment provided by engineer. + + Send errors + Fehler beim Senden + No comment provided by engineer. + Send link previews Link-Vorschau senden @@ -5019,14 +6812,29 @@ Fehler: %@ 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. @@ -5052,7 +6860,7 @@ Fehler: %@ Sender cancelled file transfer. Der Absender hat die Dateiübertragung abgebrochen. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6917,11 @@ Fehler: %@ Gesendet um: %@ copied message info + + Sent directly + Direkt gesendet + No comment provided by engineer. + Sent file event Datei-Ereignis wurde gesendet @@ -5119,11 +6932,71 @@ Fehler: %@ Gesendete Nachricht message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + Der Server wurde dem Betreiber %@ hinzugefügt. + alert message + + + 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: %@. + No comment provided by engineer. + + + Server operator changed. + Der Server-Betreiber wurde geändert. + alert title + + + Server operators + Server-Betreiber + No comment provided by engineer. + + + Server protocol changed. + Das Server-Protokoll wurde geändert. + alert title + Server requires authorization to create queues, check password Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort @@ -5139,11 +7012,36 @@ Fehler: %@ Server Test ist fehlgeschlagen! No comment provided by engineer. + + Server type + Server-Typ + No comment provided by engineer. + + + Server version is incompatible with network settings. + Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel. + srv error text + + + 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 @@ -5154,11 +7052,21 @@ Fehler: %@ 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 @@ -5169,6 +7077,11 @@ Fehler: %@ Anstelle der System-Authentifizierung festlegen. No comment provided by engineer. + + Set message expiration in chats. + Verfallsdatum von Nachrichten in Chats festlegen. + No comment provided by engineer. + Set passcode Zugangscode einstellen @@ -5199,6 +7112,11 @@ Fehler: %@ Einstellungen No comment provided by engineer. + + Settings were changed. + Die Einstellungen wurden geändert. + alert message + Shape profile images Form der Profil-Bilder @@ -5207,21 +7125,42 @@ Fehler: %@ Share Teilen - chat item action + alert action +chat item action 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 SimpleX address on social media. + Die SimpleX-Adresse auf sozialen Medien teilen. + No comment provided by engineer. + Share address Adresse teilen No comment provided by engineer. + + Share address publicly + Die Adresse öffentlich teilen + No comment provided by engineer. + Share address with contacts? Die Adresse mit Kontakten teilen? + alert title + + + Share from other apps. + Aus anderen Apps heraus teilen. No comment provided by engineer. @@ -5229,16 +7168,31 @@ Fehler: %@ Link teilen No comment provided by engineer. + + Share profile + Profil 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 QR code QR-Code anzeigen @@ -5259,21 +7213,46 @@ Fehler: %@ 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 → on messages sent via private routing. + Bei Nachrichten, die über privates Routing versendet wurden, → 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 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. @@ -5304,6 +7283,21 @@ Fehler: %@ 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? + No comment provided by engineer. + + + SimpleX channel link + SimpleX-Kanal-Link + simplex link type + SimpleX contact address SimpleX-Kontaktadressen-Link @@ -5324,8 +7318,8 @@ Fehler: %@ SimpleX-Links chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. In dieser Gruppe sind SimpleX-Links nicht erlaubt. No comment provided by engineer. @@ -5339,11 +7333,21 @@ Fehler: %@ SimpleX-Einmal-Einladung simplex link type + + 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 @@ -5359,16 +7363,49 @@ Fehler: %@ Kleine Gruppen (max. 20) No comment provided by engineer. + + Soft + Weich + blur media + + + 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + Einige Server haben den Test nicht bestanden: +%@ + alert message + Somebody Jemand notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Quadratisch, kreisförmig oder irgendetwas dazwischen. @@ -5389,6 +7426,16 @@ Fehler: %@ Starten Sie die Migration No comment provided by engineer. + + Starting from %@. + Beginnend mit %@. + No comment provided by engineer. + + + Statistics + Statistiken + No comment provided by engineer. + Stop Beenden @@ -5404,11 +7451,6 @@ Fehler: %@ Chat beenden No comment provided by engineer. - - Stop chat to enable database actions - Chat beenden, um Datenbankaktionen zu erlauben - 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. @@ -5421,44 +7463,79 @@ Fehler: %@ Stop file - Datei beenden + Herunterladen beenden cancel file action Stop receiving file? - Den Empfang der Datei beenden? + Das Herunterladen der Datei beenden? No comment provided by engineer. Stop sending file? - Das Senden der Datei beenden? + Das Hochladen der Datei beenden? No comment provided by engineer. Stop sharing Teilen beenden - No comment provided by engineer. + alert action Stop sharing address? Das Teilen der Adresse beenden? - No comment provided by engineer. + alert title Stopping chat Chat wird beendet No comment provided by engineer. + + Storage + Ablage + No comment provided by engineer. + + + Strong + Hart + blur media + 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 @@ -5469,11 +7546,21 @@ Fehler: %@ System-Authentifizierung 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 @@ -5489,11 +7576,21 @@ Fehler: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Sprechblasen-Format + No comment provided by engineer. + Take picture Machen Sie ein Foto No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. + No comment provided by engineer. + Tap button Schaltfläche antippen @@ -5529,16 +7626,21 @@ Fehler: %@ Zum Scannen tippen No comment provided by engineer. - - Tap to start a new chat - Zum Starten eines neuen Chats tippen - No comment provided by engineer. + + Temporary file error + Temporärer Datei-Fehler + file error alert title Test failed at step %@. Der Test ist beim Schritt %@ fehlgeschlagen. server test failure + + Test notifications + Benachrichtigungen testen + No comment provided by engineer. + Test server Teste Server @@ -5552,7 +7654,7 @@ Fehler: %@ Tests failed! Tests sind fehlgeschlagen! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7671,6 @@ Fehler: %@ 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 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. @@ -5586,6 +7683,16 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro 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. @@ -5596,6 +7703,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, 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! @@ -5616,6 +7728,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Die Verschlüsselung funktioniert und ein neues Verschlüsselungsabkommen ist nicht erforderlich. Es kann zu Verbindungsfehlern kommen! No comment provided by engineer. + + The future of messaging + Die nächste Generation von privatem Messaging + No comment provided by engineer. + The hash of the previous message is different. Der Hash der vorherigen Nachricht unterscheidet sich. @@ -5631,9 +7748,14 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro 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. @@ -5641,9 +7763,14 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden. No comment provided by engineer. - - The profile is only shared with your contacts. - Das Profil wird nur mit Ihren Kontakten geteilt. + + 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. @@ -5658,7 +7785,12 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro The servers for new connections of your current chat profile **%@**. - Mögliche Server für neue Verbindungen von Ihrem aktuellen Chat-Profil **%@**. + Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil **%@**. + No comment provided by engineer. + + + 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. @@ -5666,11 +7798,21 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Der von Ihnen eingefügte Text ist kein SimpleX-Link. No comment provided by engineer. - - Theme + + 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 **%@**. @@ -5683,17 +7825,22 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro 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. + 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. + 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! No comment provided by engineer. + + 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! + alert message + 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. + Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -5736,11 +7883,31 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Das ist Ihr eigener Einmal-Link! 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. + + Title + Bezeichnung + No comment provided by engineer. + To ask any questions and to receive updates: Um Fragen zu stellen und aktuelle Informationen zu erhalten: @@ -5761,9 +7928,9 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro 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. @@ -5771,6 +7938,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,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. 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. @@ -5788,26 +7980,61 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt 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. No comment provided by engineer. + + Token status: %@. + Token-Status: %@. + token status + + + 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: %@). @@ -5863,10 +8090,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Mitglied freigeben? No comment provided by engineer. - - Unexpected error: %@ - Unerwarteter Fehler: %@ - item status description + + Undelivered messages + Nicht ausgelieferte Nachrichten + No comment provided by engineer. Unexpected migration state @@ -5876,7 +8103,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Unfav. Fav. entf. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +8140,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Unbekannter Fehler No comment provided by engineer. + + Unknown servers! + Unbekannte Server! + alert title + 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. @@ -5948,11 +8180,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Unmute Stummschaltung aufheben - No comment provided by engineer. + notification label action Unread Ungelesen + swipe action + + + Unsupported connection link + Verbindungs-Link wird nicht unterstützt No comment provided by engineer. @@ -5965,11 +8202,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Aktualisieren No comment provided by engineer. - - Update .onion hosts setting? - Einstellung für .onion-Hosts aktualisieren? - No comment provided by engineer. - Update database passphrase Datenbank-Passwort aktualisieren @@ -5980,9 +8212,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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 No comment provided by engineer. @@ -5990,16 +8227,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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. - No comment provided by engineer. - Upgrade and open chat Aktualisieren und den Chat öffnen No comment provided by engineer. + + Upload errors + Fehler beim Hochladen + No comment provided by engineer. + Upload failed Hochladen fehlgeschlagen @@ -6010,21 +8247,51 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Datei hochladen server test step + + 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 %@ + Verwende %@ + No comment provided by engineer. + Use .onion hosts Verwende .onion-Hosts No comment provided by engineer. + + Use SOCKS proxy + SOCKS-Proxy nutzen + No comment provided by engineer. + Use SimpleX Chat servers? Verwenden Sie SimpleX-Chat-Server? No comment provided by engineer. + + 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 chat Verwenden Sie Chat @@ -6032,7 +8299,17 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use current profile - Das aktuelle Profil nutzen + 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. @@ -6052,7 +8329,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use new incognito profile - Ein neues Inkognito-Profil nutzen + Neues Inkognito-Profil nutzen No comment provided by engineer. @@ -6060,24 +8337,54 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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 the app while in the call. Die App kann während eines Anrufs genutzt werden. No comment provided by engineer. - - User profile - Benutzerprofil + + Use the app with one hand. + Die App mit einer Hand bedienen. 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 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. @@ -6137,12 +8444,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Video will be received when your contact completes uploading it. - Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat. + 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! + Das Video wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später! No comment provided by engineer. @@ -6150,11 +8457,21 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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 + No comment provided by engineer. + Visible history Sichtbarer Nachrichtenverlauf @@ -6170,8 +8487,8 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s In diesem Chat sind Sprachnachrichten nicht erlaubt. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. In dieser Gruppe sind Sprachnachrichten nicht erlaubt. No comment provided by engineer. @@ -6210,6 +8527,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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. + 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 @@ -6255,9 +8582,9 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Bei der Verbindung über Audio- und Video-Anrufe. 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 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. @@ -6295,26 +8622,41 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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. + No comment provided by engineer. + + + 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: %@. + alert message + Wrong database passphrase Falsches Datenbank-Passwort No comment provided by engineer. + + 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. + snd 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. + file error text + Wrong passphrase! Falsches Passwort! No comment provided by engineer. - - XFTP servers + + XFTP server XFTP-Server No comment provided by engineer. - - 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. @@ -6340,6 +8682,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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. @@ -6387,11 +8734,26 @@ Verbindungsanfrage wiederholen? 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 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 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 @@ -6422,11 +8784,21 @@ Verbindungsanfrage wiederholen? Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Sie können nun Nachrichten an %@ versenden notification body + + 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. @@ -6442,16 +8814,16 @@ Verbindungsanfrage wiederholen? 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. - 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 No comment provided by engineer. + + 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. @@ -6465,23 +8837,23 @@ Verbindungsanfrage wiederholen? 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. + alert message You can't send messages! Sie können keine Nachrichten versenden! 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. - No comment provided by engineer. - You could not be verified; please try again. Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut. No comment provided by engineer. + + You decide who can connect. + Sie entscheiden, wer sich mit Ihnen verbinden kann. + No comment provided by engineer. + You have already requested connection via this address! Sie haben über diese Adresse bereits eine Verbindung beantragt! @@ -6494,11 +8866,6 @@ Repeat connection request? Verbindungsanfrage 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. @@ -6519,11 +8886,26 @@ Verbindungsanfrage wiederholen? Sie sind dieser Gruppe beigetreten. Sie werden mit dem einladenden Gruppenmitglied verbunden. 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. @@ -6539,6 +8921,11 @@ Verbindungsanfrage wiederholen? Sie haben eine Gruppeneinladung gesendet No comment provided by engineer. + + You should receive notifications. + Sie sollten Benachrichtigungen erhalten. + token info + 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! @@ -6574,6 +8961,11 @@ Verbindungsanfrage wiederholen? Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind. No comment provided by engineer. + + You will stop receiving messages from this 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 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. @@ -6594,31 +8986,16 @@ Verbindungsanfrage wiederholen? 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 ICE servers Ihre ICE-Server No comment provided by engineer. - - Your SMP servers - Ihre SMP-Server - No comment provided by engineer. - Your SimpleX address Ihre SimpleX-Adresse No comment provided by engineer. - - Your XFTP servers - Ihre XFTP-Server - No comment provided by engineer. - Your calls Anrufe @@ -6634,16 +9011,19 @@ Verbindungsanfrage wiederholen? Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen. No comment provided by engineer. + + Your chat preferences + Ihre Chat-Präferenzen + alert title + Your chat profiles Ihre Chat-Profile No comment provided by engineer. - - 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). - 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). + + 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. @@ -6661,6 +9041,11 @@ Sie können diese Verbindung abbrechen und den Kontakt entfernen (und es später Ihre Kontakte bleiben weiterhin verbunden. No comment provided by engineer. + + 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. @@ -6678,7 +9063,7 @@ Sie können diese Verbindung abbrechen und den Kontakt entfernen (und es später Your privacy - Ihre Privatsphäre + Privatsphäre No comment provided by engineer. @@ -6691,33 +9076,36 @@ Sie können diese Verbindung abbrechen und den Kontakt entfernen (und es später Ihr Profil **%@** wird geteilt. No comment provided by engineer. - - 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. + + 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. - - Your profile, contacts and delivered messages are stored on your device. - Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert. + + 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. No comment provided by engineer. + + 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. + alert message + 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 servers + Ihre Server + No comment provided by engineer. + Your settings Einstellungen @@ -6758,6 +9146,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Anruf angenommen call status + + accepted invitation + Einladung angenommen + chat list item title + admin Admin @@ -6793,6 +9186,16 @@ SimpleX-Server können Ihr Profil nicht einsehen. und %lld weitere Ereignisse No comment provided by engineer. + + archived report + Archivierte Meldung + No comment provided by engineer. + + + attempts + Versuche + No comment provided by engineer. + audio call (not e2e encrypted) Audioanruf (nicht E2E verschlüsselt) @@ -6826,13 +9229,19 @@ SimpleX-Server können Ihr Profil nicht einsehen. blocked by admin wurde vom Administrator blockiert - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold fett No comment provided by engineer. + + call + Anrufen + No comment provided by engineer. + call error Fehler bei Anruf @@ -6936,7 +9345,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. connecting… Verbinde… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +9392,16 @@ SimpleX-Server können Ihr Profil nicht einsehen. Tage time unit + + decryption errors + Entschlüsselungs-Fehler + No comment provided by engineer. + default (%@) - Voreinstellung (%@) - pref value + Default (%@) + delete after time +pref value default (no) @@ -7033,6 +9448,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Doppelte Nachricht integrity error chat item + + duplicates + Duplikate + No comment provided by engineer. + e2e encrypted E2E-verschlüsselt @@ -7108,9 +9528,9 @@ SimpleX-Server können Ihr Profil nicht einsehen. Fehler No comment provided by engineer. - - event happened - event happened + + expired + Abgelaufen No comment provided by engineer. @@ -7143,6 +9563,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. 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. + + inactive + Inaktiv + No comment provided by engineer. + incognito via contact address link Inkognito über einen Kontaktadressen-Link @@ -7183,6 +9608,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Einladung zur Gruppe %@ group name + + invite + Einladen + No comment provided by engineer. + invited eingeladen @@ -7238,6 +9668,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. ist der Gruppe beigetreten rcv group event chat item + + message + Nachricht + No comment provided by engineer. + message received Nachricht empfangen @@ -7263,6 +9698,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Von %@ moderiert marked deleted chat item preview text + + moderator + Moderator + member role + months Monate @@ -7271,7 +9711,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. never nie - No comment provided by engineer. + delete after time new message @@ -7302,8 +9742,8 @@ SimpleX-Server können Ihr Profil nicht einsehen. off Aus enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9760,16 @@ SimpleX-Server können Ihr Profil nicht einsehen. Ein group pref value + + other + Andere + No comment provided by engineer. + + + other errors + Andere Fehler + No comment provided by engineer. + owner Eigentümer @@ -7335,6 +9785,16 @@ SimpleX-Server können Ihr Profil nicht einsehen. Peer-to-Peer No comment provided by engineer. + + pending + ausstehend + No comment provided by engineer. + + + pending approval + ausstehende Genehmigung + No comment provided by engineer. + quantum resistant e2e encryption Quantum-resistente E2E-Verschlüsselung @@ -7350,6 +9810,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Bestätigung erhalten… No comment provided by engineer. + + rejected + abgelehnt + No comment provided by engineer. + rejected call Abgelehnter Anruf @@ -7380,6 +9845,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. hat Sie aus der Gruppe entfernt rcv group event chat item + + requested to connect + Zur Verbindung aufgefordert + chat list item title + saved abgespeichert @@ -7390,6 +9860,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. abgespeichert von %@ No comment provided by engineer. + + search + Suchen + No comment provided by engineer. + sec sek @@ -7415,6 +9890,15 @@ SimpleX-Server können Ihr Profil nicht einsehen. Direktnachricht senden No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + Server-Warteschlangen-Information: %1$@ + +Zuletzt empfangene Nachricht: %2$@ + queue info + set new contact address Es wurde eine neue Kontaktadresse festgelegt @@ -7455,11 +9939,21 @@ SimpleX-Server können Ihr Profil nicht einsehen. Unbekannt connection info + + unknown servers + Unbekannte Relais + No comment provided by engineer. + unknown status unbekannter Gruppenmitglieds-Status No comment provided by engineer. + + unprotected + Ungeschützt + No comment provided by engineer. + updated group profile Aktualisiertes Gruppenprofil @@ -7500,6 +9994,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. über Relais No comment provided by engineer. + + video + Video + No comment provided by engineer. + video call (not e2e encrypted) Videoanruf (nicht E2E verschlüsselt) @@ -7525,6 +10024,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Wochen time unit + + when IP hidden + Wenn die IP-Adresse versteckt ist + No comment provided by engineer. + yes Ja @@ -7609,7 +10113,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.
- +
@@ -7619,7 +10123,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Nutzern zu scannen und Videoanrufe durchzuführen. + SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Benutzern zu scannen und Videoanrufe durchzuführen. Privacy - Camera Usage Description @@ -7639,14 +10143,14 @@ SimpleX-Server können Ihr Profil nicht einsehen. SimpleX needs access to Photo Library for saving captured and received media - SimpleX benötigt Zugriff auf das Fotoalbum, um selbst gemachte oder empfangene Bilder zu speichern + SimpleX benötigt Zugriff auf das Fotoalbum, um selbst gemachte oder heruntergeladene Bilder zu speichern Privacy - Photo Library Additions Usage Description
- +
@@ -7661,9 +10165,255 @@ SimpleX-Server können Ihr Profil nicht einsehen. Copyright © 2022 SimpleX Chat. All rights reserved. - Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. All rights reserved. Copyright (human-readable)
+ +
+ +
+ + + %d new events + %d neue Ereignisse + notification body + + + From %d chat(s) + Von %d Chat(s) + notification body + + + From: %@ + Von: %@ + notification body + + + New events + Neue Ereignisse + notification + + + New messages + Neue Nachrichten + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2025 SimpleX Chat. Alle Rechte vorbehalten. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 18051ae350..fc1846942c 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -186,20 +186,16 @@ Available in v5.1 ) No comment provided by engineer.
- - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - **Create link / QR code** for your contact to use. 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. + + **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. 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). + + **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. No comment provided by engineer. @@ -210,8 +206,8 @@ Available in v5.1 **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. + + **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. No comment provided by engineer. @@ -336,8 +332,8 @@ Available in v5.1 Add servers by scanning QR codes. No comment provided by engineer. - - Add server… + + Add server No comment provided by engineer. @@ -1128,8 +1124,8 @@ Available in v5.1 Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1144,8 +1140,8 @@ Available in v5.1 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. @@ -1580,16 +1576,16 @@ Available in v5.1 Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1708,8 +1704,8 @@ Available in v5.1 Immediately No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1821,8 +1817,8 @@ Available in v5.1 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. @@ -2016,8 +2012,8 @@ Available in v5.1 Migration is completed No comment provided by engineer. - - Migrations: %@ + + Migrations: No comment provided by engineer. @@ -2174,8 +2170,8 @@ Available in v5.1 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**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2234,8 +2230,8 @@ Available in v5.1 Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2290,8 +2286,8 @@ Available in v5.1 Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2994,8 +2990,8 @@ Available in v5.1 Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -3039,16 +3035,16 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of 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. @@ -3111,8 +3107,8 @@ It can happen because of some bug or when the connection is compromised.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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3337,8 +3333,8 @@ To connect, please ask your contact to create another connection link and check 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. @@ -3478,10 +3474,6 @@ SimpleX Lock must be enabled. You can't send messages! 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. @@ -4200,7 +4192,7 @@ SimpleX servers cannot see your profile. ## In reply to - ## Ως απαντηση σε + ## Ως απάντηση σε copied message info 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 2b5677dd61..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 @@
- +
- - - - - - 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) (can be copied) @@ -127,6 +100,11 @@ %@ is verified No comment provided by engineer. + + %@ server + %@ server + No comment provided by engineer. + %@ servers %@ servers @@ -142,6 +120,11 @@ %@ wants to connect! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ and %lld members @@ -162,11 +145,36 @@ %d days time interval + + %d file(s) are still being downloaded. + %d file(s) are still being downloaded. + forward confirmation reason + + + %d file(s) failed to download. + %d file(s) failed to download. + forward confirmation reason + + + %d file(s) were deleted. + %d file(s) were deleted. + forward confirmation reason + + + %d file(s) were not downloaded. + %d file(s) were not downloaded. + forward confirmation reason + %d hours %d hours time interval + + %d messages not forwarded + %d messages not forwarded + alert title + %d min %d min @@ -182,6 +190,11 @@ %d sec time interval + + %d seconds(s) + %d seconds(s) + delete after time + %d skipped message(s) %d skipped message(s) @@ -252,11 +265,6 @@ %lld new interface languages No comment provided by engineer. - - %lld second(s) - %lld second(s) - No comment provided by engineer. - %lld seconds %lld seconds @@ -307,11 +315,6 @@ %u messages skipped. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (new) @@ -322,19 +325,9 @@ (this device v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Add new contact**: to create your one-time QR Code or link for your contact. + + **Create 1-time link**: to create and share a new invitation link. + **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. @@ -342,14 +335,14 @@ **Create group**: to create a new group. 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. - **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. + + **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. + **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. 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). - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the 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. + **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. No comment provided by engineer. @@ -362,9 +355,14 @@ **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. - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **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. + **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. + No comment provided by engineer. + + + **Scan / Paste link**: to connect via a link you received. + **Scan / Paste link**: to connect via a link you received. No comment provided by engineer. @@ -392,11 +390,6 @@ \*bold* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +426,6 @@ - editing history. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -451,7 +439,8 @@ 1 day 1 day - time interval + delete after time +time interval 1 hour @@ -466,12 +455,29 @@ 1 month 1 month - time interval + delete after time +time interval 1 week 1 week - time interval + delete after time +time interval + + + 1 year + 1 year + delete after time + + + 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. + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. 5 minutes @@ -488,11 +494,6 @@ 30 seconds No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -542,31 +543,32 @@ Abort changing address? No comment provided by engineer. - - About SimpleX - About SimpleX - No comment provided by engineer. - About SimpleX Chat About SimpleX Chat No comment provided by engineer. - - About SimpleX address - About SimpleX address + + About operators + About operators No comment provided by engineer. - - Accent color - Accent color + + Accent + Accent No comment provided by engineer. Accept Accept accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Accept conditions + No comment provided by engineer. Accept connection request? @@ -581,21 +583,47 @@ Accept incognito Accept incognito - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Accepted conditions + No comment provided by engineer. + + + Acknowledged + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + Acknowledgement errors + No comment provided by engineer. + + + Active + Active + token status text + + + Active connections + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. - - Add contact - Add contact + + Add friends + Add friends No comment provided by engineer. - - Add preset servers - Add preset servers + + Add list + Add list No comment provided by engineer. @@ -603,14 +631,19 @@ Add profile No comment provided by engineer. + + Add server + Add server + No comment provided by engineer. + Add servers by scanning QR codes. Add servers by scanning QR codes. No comment provided by engineer. - - Add server… - Add server… + + Add team members + Add team members No comment provided by engineer. @@ -618,11 +651,46 @@ Add to another device No comment provided by engineer. + + Add to list + Add to list + No comment provided by engineer. + Add welcome message Add welcome message No comment provided by engineer. + + Add your team members to the conversations. + Add your team members to the conversations. + No comment provided by engineer. + + + Added media & file servers + Added media & file servers + No comment provided by engineer. + + + Added message servers + Added message servers + No comment provided by engineer. + + + Additional accent + Additional accent + No comment provided by engineer. + + + Additional accent 2 + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + Additional secondary + No comment provided by engineer. + Address Address @@ -633,6 +701,16 @@ Address change will be aborted. Old receiving address will be used. No comment provided by engineer. + + Address or 1-time link? + Address or 1-time link? + No comment provided by engineer. + + + Address settings + Address settings + No comment provided by engineer. + Admins can block a member for all. Admins can block a member for all. @@ -648,6 +726,16 @@ Advanced network settings No comment provided by engineer. + + Advanced settings + Advanced settings + No comment provided by engineer. + + + All + All + No comment provided by engineer. + All app data is deleted. All app data is deleted. @@ -658,16 +746,31 @@ All chats and messages will be deleted - this cannot be undone! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + All chats will be removed from the list %@, and the list deleted. + alert message + All data is erased when it is entered. All data is erased when it is entered. No comment provided by engineer. + + All data is kept private on your device. + All data is kept private on your device. + No comment provided by engineer. + All group members will remain connected. 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. + 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! All messages will be deleted - this cannot be undone! @@ -683,6 +786,21 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + All profiles + profile dropdown + + + All reports will be archived for you. + All reports will be archived for you. + No comment provided by engineer. + + + All servers + All servers + No comment provided by engineer. + All your contacts will remain connected. All your contacts will remain connected. @@ -708,11 +826,21 @@ Allow calls only if your contact allows them. No comment provided by engineer. + + Allow calls? + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Allow disappearing messages only if your contact allows it to you. No comment provided by engineer. + + Allow downgrade + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Allow irreversible message deletion only if your contact allows it to you. (24 hours) @@ -738,11 +866,21 @@ Allow sending disappearing messages. No comment provided by engineer. + + Allow sharing + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Allow to irreversibly delete sent messages. (24 hours) No comment provided by engineer. + + Allow to report messsages to moderators. + Allow to report messsages to moderators. + No comment provided by engineer. + Allow to send SimpleX links. Allow to send SimpleX links. @@ -808,6 +946,11 @@ Already joining the group! No comment provided by engineer. + + Always use private routing. + Always use private routing. + No comment provided by engineer. + Always use relay Always use relay @@ -818,11 +961,21 @@ An empty chat profile with the provided name is created, and the app opens as usual. No comment provided by engineer. + + Another reason + Another reason + report reason + Answer call Answer call No comment provided by engineer. + + Anybody can host servers. + Anybody can host servers. + No comment provided by engineer. + App build: %@ App build: %@ @@ -838,6 +991,11 @@ App encrypts new local files (except videos). No comment provided by engineer. + + App group: + App group: + No comment provided by engineer. + App icon App icon @@ -853,6 +1011,11 @@ App passcode is replaced with self-destruct passcode. No comment provided by engineer. + + App session + App session + No comment provided by engineer. + App version App version @@ -873,11 +1036,56 @@ Apply No comment provided by engineer. + + Apply to + Apply to + No comment provided by engineer. + + + Archive + Archive + No comment provided by engineer. + + + Archive %lld reports? + Archive %lld reports? + No comment provided by engineer. + + + Archive all reports? + Archive all reports? + No comment provided by engineer. + Archive and upload Archive and upload No comment provided by engineer. + + Archive contacts to chat later. + Archive contacts to chat later. + No comment provided by engineer. + + + Archive report + Archive report + No comment provided by engineer. + + + Archive report? + Archive report? + No comment provided by engineer. + + + Archive reports + Archive reports + swipe action + + + Archived contacts + Archived contacts + No comment provided by engineer. + Archiving database Archiving database @@ -943,11 +1151,21 @@ Auto-accept images No comment provided by engineer. + + Auto-accept settings + Auto-accept settings + alert title + Back Back No comment provided by engineer. + + Background + Background + No comment provided by engineer. + Bad desktop address Bad desktop address @@ -963,16 +1181,61 @@ Bad message hash No comment provided by engineer. + + Better calls + Better calls + No comment provided by engineer. + Better groups Better groups No comment provided by engineer. + + Better groups performance + Better groups performance + No comment provided by engineer. + + + Better message dates. + Better message dates. + No comment provided by engineer. + Better messages Better messages No comment provided by engineer. + + Better networking + Better networking + No comment provided by engineer. + + + Better notifications + Better notifications + No comment provided by engineer. + + + Better privacy and security + Better privacy and security + No comment provided by engineer. + + + Better security ✅ + Better security ✅ + No comment provided by engineer. + + + Better user experience + Better user experience + No comment provided by engineer. + + + Black + Black + No comment provided by engineer. + Block Block @@ -1008,6 +1271,16 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + Blur for better privacy. + No comment provided by engineer. + + + Blur media + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Both you and your contact can add message reactions. @@ -1038,11 +1311,35 @@ Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + Business address + No comment provided by engineer. + + + Business chats + Business chats + No comment provided by engineer. + + + Businesses + 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). By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! Call already ended! @@ -1053,11 +1350,26 @@ Calls No comment provided by engineer. + + Calls prohibited! + Calls prohibited! + No comment provided by engineer. + Camera not available Camera not available No comment provided by engineer. + + Can't call contact + Can't call contact + No comment provided by engineer. + + + Can't call member + Can't call member + No comment provided by engineer. + Can't invite contact! Can't invite contact! @@ -1068,10 +1380,16 @@ Can't invite contacts! No comment provided by engineer. + + Can't message member + Can't message member + No comment provided by engineer. + Cancel Cancel - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1401,20 @@ Cannot access keychain to save database password No comment provided by engineer. + + Cannot forward message + Cannot forward message + No comment provided by engineer. + Cannot receive file Cannot receive file - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Capacity exceeded - recipient did not receive previously sent messages. + snd error text Cellular @@ -1098,6 +1426,16 @@ Change No comment provided by engineer. + + Change automatic message deletion? + Change automatic message deletion? + alert title + + + Change chat profiles + Change chat profiles + authentication reason + Change database passphrase? Change database passphrase? @@ -1142,11 +1480,26 @@ Change self-destruct passcode Change self-destruct passcode authentication reason - set passcode view +set passcode view - - Chat archive - Chat archive + + Chat + Chat + No comment provided by engineer. + + + Chat already exists + Chat already exists + No comment provided by engineer. + + + Chat already exists! + Chat already exists! + No comment provided by engineer. + + + Chat colors + Chat colors No comment provided by engineer. @@ -1164,6 +1517,11 @@ Chat database deleted No comment provided by engineer. + + Chat database exported + Chat database exported + No comment provided by engineer. + Chat database imported Chat database imported @@ -1184,6 +1542,11 @@ 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 + Chat list + No comment provided by engineer. + Chat migrated! Chat migrated! @@ -1194,15 +1557,50 @@ Chat preferences No comment provided by engineer. + + Chat preferences were changed. + Chat preferences were changed. + alert message + + + Chat profile + Chat profile + No comment provided by engineer. + + + Chat theme + Chat theme + No comment provided by engineer. + + + Chat will be deleted for all members - this cannot be undone! + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Chats No comment provided by engineer. + + Check messages every 20 min. + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Check server address and try again. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1622,25 @@ Choose from library No comment provided by engineer. + + Chunks deleted + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + Chunks uploaded + No comment provided by engineer. + Clear Clear - No comment provided by engineer. + swipe action Clear conversation @@ -1239,6 +1652,16 @@ Clear conversation? No comment provided by engineer. + + Clear group? + Clear group? + No comment provided by engineer. + + + Clear or delete group? + Clear or delete group? + No comment provided by engineer. + Clear private notes? Clear private notes? @@ -1249,11 +1672,21 @@ Clear verification No comment provided by engineer. - - Colors - Colors + + Color chats with the new themes. + Color chats with the new themes. No comment provided by engineer. + + Color mode + Color mode + No comment provided by engineer. + + + Community guidelines violation + Community guidelines violation + report reason + Compare file Compare file @@ -1264,11 +1697,56 @@ Compare security codes with your contacts. No comment provided by engineer. + + Completed + Completed + No comment provided by engineer. + + + Conditions accepted on: %@. + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for these operator(s): **%@**. + Conditions are already accepted for these operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Configure ICE servers No comment provided by engineer. + + Configure server operators + Configure server operators + No comment provided by engineer. + Confirm Confirm @@ -1279,11 +1757,21 @@ Confirm Passcode No comment provided by engineer. + + Confirm contact deletion? + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades Confirm database upgrades No comment provided by engineer. + + Confirm files from unknown servers. + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Confirm network settings @@ -1309,6 +1797,11 @@ Confirm upload No comment provided by engineer. + + Confirmed + Confirmed + token status text + Connect Connect @@ -1329,6 +1822,11 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? Connect to yourself? @@ -1368,16 +1866,31 @@ This is your own one-time link! Connect with %@ No comment provided by engineer. + + Connected + Connected + No comment provided by engineer. + Connected desktop Connected desktop No comment provided by engineer. + + Connected servers + Connected servers + No comment provided by engineer. + Connected to desktop Connected to desktop No comment provided by engineer. + + Connecting + Connecting + No comment provided by engineer. + Connecting to server… Connecting to server… @@ -1388,6 +1901,11 @@ This is your own one-time link! Connecting to server… (error: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop Connecting to desktop @@ -1398,6 +1916,16 @@ This is your own one-time link! Connection No comment provided by engineer. + + Connection and servers status. + Connection and servers status. + No comment provided by engineer. + + + Connection blocked + Connection blocked + No comment provided by engineer. + Connection error Connection error @@ -1408,11 +1936,38 @@ This is your own one-time link! Connection error (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + Connection is blocked by server operator: +%@ + No comment provided by engineer. + + + Connection not ready. + Connection not ready. + No comment provided by engineer. + + + Connection notifications + Connection notifications + No comment provided by engineer. + Connection request sent! Connection request sent! No comment provided by engineer. + + Connection requires encryption renegotiation. + Connection requires encryption renegotiation. + No comment provided by engineer. + + + Connection security + Connection security + No comment provided by engineer. + Connection terminated Connection terminated @@ -1423,6 +1978,16 @@ This is your own one-time link! Connection timeout No comment provided by engineer. + + Connection with desktop stopped + Connection with desktop stopped + No comment provided by engineer. + + + Connections + Connections + No comment provided by engineer. + Contact allows Contact allows @@ -1433,6 +1998,11 @@ This is your own one-time link! Contact already exists No comment provided by engineer. + + Contact deleted! + Contact deleted! + No comment provided by engineer. + Contact hidden: Contact hidden: @@ -1443,9 +2013,9 @@ This is your own one-time link! Contact is connected notification - - Contact is not connected yet! - Contact is not connected yet! + + Contact is deleted. + Contact is deleted. No comment provided by engineer. @@ -1458,6 +2028,11 @@ This is your own one-time link! Contact preferences No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Contacts @@ -1468,21 +2043,41 @@ This is your own one-time link! Contacts can mark messages for deletion; you will be able to view them. No comment provided by engineer. + + Content violates conditions of use + Content violates conditions of use + blocking reason + Continue Continue No comment provided by engineer. + + Conversation deleted! + Conversation deleted! + No comment provided by engineer. + Copy Copy - chat item action + No comment provided by engineer. + + + Copy error + Copy error + No comment provided by engineer. Core version: v%@ Core version: v%@ No comment provided by engineer. + + Corner + Corner + No comment provided by engineer. + Correct name to %@? Correct name to %@? @@ -1493,6 +2088,11 @@ This is your own one-time link! Create No comment provided by engineer. + + Create 1-time link + Create 1-time link + No comment provided by engineer. + Create SimpleX address Create SimpleX address @@ -1503,11 +2103,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - Create an address to let people connect with you. - No comment provided by engineer. - Create file Create file @@ -1528,6 +2123,11 @@ This is your own one-time link! Create link No comment provided by engineer. + + Create list + Create list + No comment provided by engineer. + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 @@ -1553,6 +2153,11 @@ This is your own one-time link! Create your profile No comment provided by engineer. + + Created + Created + No comment provided by engineer. + Created at Created at @@ -1563,11 +2168,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - Created on %@ - No comment provided by engineer. - Creating archive link Creating archive link @@ -1583,11 +2183,21 @@ This is your own one-time link! Current Passcode No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Current passphrase… No comment provided by engineer. + + Current profile + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. Currently maximum supported file size is %@. @@ -1598,11 +2208,26 @@ This is your own one-time link! Custom time No comment provided by engineer. + + Customizable message shape. + Customizable message shape. + No comment provided by engineer. + + + Customize theme + Customize theme + No comment provided by engineer. + Dark Dark No comment provided by engineer. + + Dark mode colors + Dark mode colors + No comment provided by engineer. + Database ID Database ID @@ -1701,6 +2326,11 @@ This is your own one-time link! Database will be migrated when the app restarts No comment provided by engineer. + + Debug delivery + Debug delivery + No comment provided by engineer. + Decentralized Decentralized @@ -1714,18 +2344,19 @@ This is your own one-time link! Delete Delete - chat item action + alert action +swipe action + + + Delete %lld messages of members? + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? Delete %lld messages? No comment provided by engineer. - - Delete Contact - Delete Contact - No comment provided by engineer. - Delete address Delete address @@ -1751,14 +2382,14 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. - - Delete archive - Delete archive + + Delete chat + Delete chat No comment provided by engineer. - - Delete chat archive? - Delete chat archive? + + Delete chat messages from your device. + Delete chat messages from your device. No comment provided by engineer. @@ -1771,6 +2402,11 @@ This is your own one-time link! Delete chat profile? No comment provided by engineer. + + Delete chat? + Delete chat? + No comment provided by engineer. + Delete connection Delete connection @@ -1781,11 +2417,9 @@ This is your own one-time link! Delete contact No comment provided by engineer. - - Delete contact? -This cannot be undone! - Delete contact? -This cannot be undone! + + Delete contact? + Delete contact? No comment provided by engineer. @@ -1848,6 +2482,11 @@ This cannot be undone! Delete link? No comment provided by engineer. + + Delete list? + Delete list? + alert title + Delete member message? Delete member message? @@ -1861,7 +2500,7 @@ This cannot be undone! Delete messages Delete messages - No comment provided by engineer. + alert button Delete messages after @@ -1878,9 +2517,9 @@ This cannot be undone! Delete old database? No comment provided by engineer. - - Delete pending connection - Delete pending connection + + Delete or moderate up to 200 messages. + Delete or moderate up to 200 messages. No comment provided by engineer. @@ -1898,11 +2537,31 @@ This cannot be undone! Delete queue server test step + + Delete report + Delete report + No comment provided by engineer. + + + Delete up to 20 messages at once. + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Delete user profile? No comment provided by engineer. + + Delete without notification + Delete without notification + No comment provided by engineer. + + + Deleted + Deleted + No comment provided by engineer. + Deleted at Deleted at @@ -1913,6 +2572,16 @@ This cannot be undone! Deleted at: %@ copied message info + + Deletion errors + Deletion errors + No comment provided by engineer. + + + Delivered even when Apple drops them. + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Delivery @@ -1948,11 +2617,41 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + Destination server error: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + Detailed statistics + No comment provided by engineer. + + + Details + Details + No comment provided by engineer. + Develop Develop No comment provided by engineer. + + Developer options + Developer options + No comment provided by engineer. + Developer tools Developer tools @@ -1983,9 +2682,14 @@ This cannot be undone! Direct messages chat feature - - Direct messages between members are prohibited in this group. - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited in this chat. + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + + + Direct messages between members are prohibited. + Direct messages between members are prohibited. No comment provided by engineer. @@ -1998,11 +2702,26 @@ This cannot be undone! Disable SimpleX Lock authentication reason + + Disable automatic message deletion? + Disable automatic message deletion? + alert title + + + Disable delete messages + Disable delete messages + alert button + Disable for all Disable for all No comment provided by engineer. + + Disabled + Disabled + No comment provided by engineer. + Disappearing message Disappearing message @@ -2018,9 +2737,9 @@ This cannot be undone! Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. + Disappearing messages are prohibited. No comment provided by engineer. @@ -2053,11 +2772,21 @@ This cannot be undone! Discover via local network No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + Do NOT send messages directly, even if your or destination server does not support private routing. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. Do NOT use SimpleX for emergency calls. No comment provided by engineer. + + Do NOT use private routing. + Do NOT use private routing. + No comment provided by engineer. + Do it later Do it later @@ -2068,6 +2797,16 @@ This cannot be undone! Do not send history to new members. No comment provided by engineer. + + Do not use credentials with proxy. + Do not use credentials with proxy. + No comment provided by engineer. + + + Documents: + Documents: + No comment provided by engineer. + Don't create address Don't create address @@ -2078,11 +2817,21 @@ This cannot be undone! Don't enable No comment provided by engineer. + + Don't miss important messages. + Don't miss important messages. + No comment provided by engineer. + Don't show again Don't show again No comment provided by engineer. + + Done + Done + No comment provided by engineer. + Downgrade and open chat Downgrade and open chat @@ -2091,7 +2840,13 @@ This cannot be undone! Download Download - chat item action + alert button +chat item action + + + Download errors + Download errors + No comment provided by engineer. Download failed @@ -2103,6 +2858,21 @@ This cannot be undone! Download file server test step + + Download files + Download files + alert action + + + Downloaded + Downloaded + No comment provided by engineer. + + + Downloaded files + Downloaded files + No comment provided by engineer. + Downloading archive Downloading archive @@ -2123,6 +2893,11 @@ This cannot be undone! Duration No comment provided by engineer. + + E2E encrypted notifications. + E2E encrypted notifications. + No comment provided by engineer. + Edit Edit @@ -2143,6 +2918,11 @@ This cannot be undone! Enable (keep overrides) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock Enable SimpleX Lock @@ -2156,7 +2936,7 @@ This cannot be undone! Enable automatic message deletion? Enable automatic message deletion? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2983,11 @@ This cannot be undone! Enable self-destruct passcode set passcode view + + Enabled + Enabled + No comment provided by engineer. + Enabled for Enabled for @@ -2278,6 +3063,11 @@ This cannot be undone! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Enter Passcode @@ -2343,26 +3133,36 @@ This cannot be undone! Error aborting address change No comment provided by engineer. + + Error accepting conditions + Error accepting conditions + alert title + Error accepting contact request Error accepting contact request No comment provided by engineer. - - Error accessing database file - Error accessing database file - No comment provided by engineer. - Error adding member(s) Error adding member(s) No comment provided by engineer. + + Error adding server + Error adding server + alert title + Error changing address Error changing address No comment provided by engineer. + + Error changing connection profile + Error changing connection profile + No comment provided by engineer. + Error changing role Error changing role @@ -2373,6 +3173,21 @@ This cannot be undone! Error changing setting No comment provided by engineer. + + Error changing to incognito! + Error changing to incognito! + No comment provided by engineer. + + + Error checking token status + Error checking token status + No comment provided by engineer. + + + Error connecting to forwarding server %@. Please try later. + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Error creating address @@ -2388,6 +3203,11 @@ This cannot be undone! Error creating group link No comment provided by engineer. + + Error creating list + Error creating list + alert title + Error creating member contact Error creating member contact @@ -2403,6 +3223,11 @@ This cannot be undone! Error creating profile! No comment provided by engineer. + + Error creating report + Error creating report + No comment provided by engineer. + Error decrypting file Error decrypting file @@ -2423,11 +3248,6 @@ This cannot be undone! Error deleting connection No comment provided by engineer. - - Error deleting contact - Error deleting contact - No comment provided by engineer. - Error deleting database Error deleting database @@ -2473,6 +3293,11 @@ This cannot be undone! Error exporting chat database No comment provided by engineer. + + Error exporting theme: %@ + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Error importing chat database @@ -2483,9 +3308,14 @@ This cannot be undone! Error joining group No comment provided by engineer. - - Error loading %@ servers - Error loading %@ servers + + Error loading servers + Error loading servers + alert title + + + Error migrating settings + Error migrating settings No comment provided by engineer. @@ -2496,16 +3326,36 @@ This cannot be undone! Error receiving file Error receiving file + alert title + + + Error reconnecting server + Error reconnecting server No comment provided by engineer. + + Error reconnecting servers + Error reconnecting servers + No comment provided by engineer. + + + Error registering for notifications + Error registering for notifications + alert title + Error removing member Error removing member No comment provided by engineer. - - Error saving %@ servers - Error saving %@ servers + + Error reordering lists + Error reordering lists + alert title + + + Error resetting statistics + Error resetting statistics No comment provided by engineer. @@ -2513,6 +3363,11 @@ This cannot be undone! Error saving ICE servers No comment provided by engineer. + + Error saving chat list + Error saving chat list + alert title + Error saving group profile Error saving group profile @@ -2528,6 +3383,11 @@ This cannot be undone! Error saving passphrase to keychain No comment provided by engineer. + + Error saving servers + Error saving servers + alert title + Error saving settings Error saving settings @@ -2573,16 +3433,26 @@ This cannot be undone! Error stopping chat No comment provided by engineer. + + Error switching profile + Error switching profile + No comment provided by engineer. + Error switching profile! Error switching profile! - No comment provided by engineer. + alertTitle Error synchronizing connection Error synchronizing connection No comment provided by engineer. + + Error testing server connection + Error testing server connection + No comment provided by engineer. + Error updating group link Error updating group link @@ -2593,6 +3463,11 @@ This cannot be undone! Error updating message No comment provided by engineer. + + Error updating server + Error updating server + alert title + Error updating settings Error updating settings @@ -2621,7 +3496,9 @@ This cannot be undone! Error: %@ Error: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3510,16 @@ This cannot be undone! Error: no database file No comment provided by engineer. + + Errors + Errors + No comment provided by engineer. + + + Errors in servers configuration. + Errors in servers configuration. + servers error + Even when disabled in the conversation. Even when disabled in the conversation. @@ -2648,6 +3535,11 @@ This cannot be undone! Expand chat item action + + Expired + Expired + token status text + Export database Export database @@ -2658,6 +3550,11 @@ This cannot be undone! Export error: No comment provided by engineer. + + Export theme + Export theme + No comment provided by engineer. + Exported database archive. Exported database archive. @@ -2683,16 +3580,70 @@ This cannot be undone! Fast and no wait until the sender is online! No comment provided by engineer. + + Faster deletion of groups. + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Faster joining and more reliable messages. No comment provided by engineer. + + Faster sending messages. + Faster sending messages. + No comment provided by engineer. + Favorite Favorite + swipe action + + + Favorites + Favorites No comment provided by engineer. + + File error + File error + file error alert title + + + File errors: +%@ + File errors: +%@ + alert message + + + File is blocked by server operator: +%@. + File is blocked by server operator: +%@. + file error text + + + File not found - most likely file was deleted or cancelled. + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + File server error: %@ + file error text + + + File status + File status + No comment provided by engineer. + + + File status: %@ + File status: %@ + copied message info + File will be deleted from servers. File will be deleted from servers. @@ -2713,6 +3664,11 @@ This cannot be undone! File: %@ No comment provided by engineer. + + Files + Files + No comment provided by engineer. + Files & media Files & media @@ -2723,9 +3679,9 @@ This cannot be undone! Files and media chat feature - - Files and media are prohibited in this group. - Files and media are prohibited in this group. + + Files and media are prohibited. + Files and media are prohibited. No comment provided by engineer. @@ -2793,21 +3749,71 @@ This cannot be undone! Fix not supported by group member No comment provided by engineer. + + For all moderators + For all moderators + No comment provided by engineer. + + + For chat profile %@: + For chat profile %@: + servers error + For console 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. + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For me + For me + No comment provided by engineer. + + + For private routing + For private routing + No comment provided by engineer. + + + For social media + For social media + No comment provided by engineer. + Forward Forward chat item action + + Forward %d message(s)? + Forward %d message(s)? + alert title + Forward and save messages Forward and save messages No comment provided by engineer. + + Forward messages + Forward messages + alert action + + + Forward messages without files? + Forward messages without files? + alert message + + + Forward up to 20 messages at once. + Forward up to 20 messages at once. + No comment provided by engineer. + Forwarded Forwarded @@ -2818,6 +3824,40 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding %lld messages + Forwarding %lld messages + No comment provided by engineer. + + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop Found desktop @@ -2838,11 +3878,6 @@ This cannot be undone! Full name (optional) No comment provided by engineer. - - Full name: - Full name: - No comment provided by engineer. - Fully decentralized – visible only to members. Fully decentralized – visible only to members. @@ -2863,6 +3898,21 @@ This cannot be undone! GIFs and stickers No comment provided by engineer. + + Get notified when mentioned. + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + Good afternoon! + message preview + + + Good morning! + Good morning! + message preview + Group Group @@ -2918,41 +3968,6 @@ This cannot be undone! Group links No comment provided by engineer. - - Group members can add message reactions. - Group members can add message reactions. - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. (24 hours) - Group members can irreversibly delete sent messages. (24 hours) - No comment provided by engineer. - - - Group members can send SimpleX links. - Group members can send SimpleX links. - No comment provided by engineer. - - - Group members can send direct messages. - Group members can send direct messages. - No comment provided by engineer. - - - Group members can send disappearing messages. - Group members can send disappearing messages. - No comment provided by engineer. - - - Group members can send files and media. - Group members can send files and media. - No comment provided by engineer. - - - Group members can send voice messages. - Group members can send voice messages. - No comment provided by engineer. - Group message: Group message: @@ -2993,11 +4008,21 @@ This cannot be undone! Group will be deleted for you - this cannot be undone! No comment provided by engineer. + + Groups + Groups + No comment provided by engineer. + Help Help No comment provided by engineer. + + Help admins moderating their groups. + Help admins moderating their groups. + No comment provided by engineer. + Hidden Hidden @@ -3048,10 +4073,20 @@ This cannot be undone! How SimpleX works No comment provided by engineer. + + How it affects privacy + How it affects privacy + No comment provided by engineer. + + + How it helps privacy + How it helps privacy + No comment provided by engineer. + How it works How it works - No comment provided by engineer. + alert button How to @@ -3078,6 +4113,11 @@ This cannot be undone! ICE servers (one per line) No comment provided by engineer. + + IP address + IP address + No comment provided by engineer. + If you can't meet in person, show QR code in a video call, or share the link. If you can't meet in person, show QR code in a video call, or share the link. @@ -3118,9 +4158,9 @@ This cannot be undone! Immediately No comment provided by engineer. - - Immune to spam and abuse - Immune to spam and abuse + + Immune to spam + Immune to spam No comment provided by engineer. @@ -3143,11 +4183,23 @@ This cannot be undone! Import failed No comment provided by engineer. + + Import theme + Import theme + No comment provided by engineer. + Importing archive Importing archive No comment provided by engineer. + + Improved delivery, reduced traffic usage. +More improvements are coming soon! + Improved delivery, reduced traffic usage. +More improvements are coming soon! + No comment provided by engineer. + Improved message delivery Improved message delivery @@ -3178,6 +4230,16 @@ This cannot be undone! In-call sounds No comment provided by engineer. + + Inappropriate content + Inappropriate content + report reason + + + Inappropriate profile + Inappropriate profile + report reason + Incognito Incognito @@ -3248,6 +4310,11 @@ This cannot be undone! Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Instant + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3255,16 +4322,41 @@ This cannot be undone! No comment provided by engineer. - - Instantly - Instantly - No comment provided by engineer. - Interface Interface No comment provided by engineer. + + Interface colors + Interface colors + No comment provided by engineer. + + + Invalid + Invalid + token status text + + + Invalid (bad token) + Invalid (bad token) + token status text + + + Invalid (expired) + Invalid (expired) + token status text + + + Invalid (unregistered) + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + Invalid (wrong topic) + token status text + Invalid QR code Invalid QR code @@ -3303,7 +4395,7 @@ This cannot be undone! Invalid server address! Invalid server address! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4417,11 @@ This cannot be undone! Invite members No comment provided by engineer. + + Invite to chat + Invite to chat + No comment provided by engineer. + Invite to group Invite to group @@ -3340,9 +4437,9 @@ This cannot be undone! 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 in this group. + + Irreversible message deletion is prohibited. + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -3366,6 +4463,11 @@ This cannot be undone! 3. The connection was compromised. No comment provided by engineer. + + It protects your IP address and connections. + It protects your IP address and connections. + 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 (%@). It seems like you are already connected via this link. If it is not the case, there was an error (%@). @@ -3384,7 +4486,7 @@ This cannot be undone! Join Join - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4528,11 @@ This is your link for group %@! Keep Keep + alert action + + + Keep conversation + Keep conversation No comment provided by engineer. @@ -3436,7 +4543,7 @@ This is your link for group %@! Keep unused invitation? Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4578,16 @@ This is your link for group %@! Leave Leave + swipe action + + + Leave chat + Leave chat + No comment provided by engineer. + + + Leave chat? + Leave chat? No comment provided by engineer. @@ -3513,6 +4630,21 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + List + List + swipe action + + + List name and emoji should be different for all lists. + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + List name... + No comment provided by engineer. + Live message! Live message! @@ -3523,11 +4655,6 @@ This is your link for group %@! Live messages No comment provided by engineer. - - Local - Local - No comment provided by engineer. - Local name Local name @@ -3548,11 +4675,6 @@ This is your link for group %@! Lock mode No comment provided by engineer. - - Make a private connection - Make a private connection - No comment provided by engineer. - Make one message disappear Make one message disappear @@ -3563,21 +4685,11 @@ This is your link for group %@! Make profile private! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - 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. Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - No comment provided by engineer. - Mark deleted for everyone Mark deleted for everyone @@ -3603,11 +4715,36 @@ This is your link for group %@! Max 30 seconds, received instantly. No comment provided by engineer. + + Media & file servers + Media & file servers + No comment provided by engineer. + + + Medium + Medium + blur media + Member Member No comment provided by engineer. + + Member inactive + Member inactive + item status text + + + Member reports + Member reports + chat feature + + + Member role will be changed to "%@". All chat members will be notified. + 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. Member role will be changed to "%@". All group members will be notified. @@ -3618,11 +4755,66 @@ This is your link for group %@! 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! + 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! Member will be removed from group - this cannot be undone! No comment provided by engineer. + + Members can add message reactions. + Members can add message reactions. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Members can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Members can report messsages to moderators. + Members can report messsages to moderators. + No comment provided by engineer. + + + Members can send SimpleX links. + Members can send SimpleX links. + No comment provided by engineer. + + + Members can send direct messages. + Members can send direct messages. + No comment provided by engineer. + + + Members can send disappearing messages. + Members can send disappearing messages. + No comment provided by engineer. + + + Members can send files and media. + Members can send files and media. + No comment provided by engineer. + + + Members can send voice messages. + Members can send voice messages. + No comment provided by engineer. + + + Mention members 👋 + Mention members 👋 + No comment provided by engineer. + + + Menus + Menus + No comment provided by engineer. + Message delivery error Message delivery error @@ -3633,11 +4825,31 @@ This is your link for group %@! Message delivery receipts! No comment provided by engineer. + + Message delivery warning + Message delivery warning + item status text + Message draft Message draft No comment provided by engineer. + + Message forwarded + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + Message may be delivered later if member becomes active. + item status description + + + Message queue info + Message queue info + No comment provided by engineer. + Message reactions Message reactions @@ -3648,9 +4860,24 @@ This is your link for group %@! Message reactions are prohibited in this chat. No comment provided by engineer. - - Message reactions are prohibited in this group. - Message reactions are prohibited in this group. + + Message reactions are prohibited. + Message reactions are prohibited. + No comment provided by engineer. + + + Message reception + Message reception + No comment provided by engineer. + + + Message servers + Message servers + No comment provided by engineer. + + + Message shape + Message shape No comment provided by engineer. @@ -3658,6 +4885,16 @@ This is your link for group %@! Message source remains private. No comment provided by engineer. + + Message status + Message status + No comment provided by engineer. + + + Message status: %@ + Message status: %@ + copied message info + Message text Message text @@ -3683,6 +4920,26 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + Messages in this chat will never be deleted. + alert message + + + Messages received + Messages received + No comment provided by engineer. + + + Messages sent + Messages sent + No comment provided by engineer. + + + Messages were deleted after you selected them. + Messages were deleted after you selected them. + alert message + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. @@ -3748,9 +5005,9 @@ This is your link for group %@! Migration is completed No comment provided by engineer. - - Migrations: %@ - Migrations: %@ + + Migrations: + Migrations: No comment provided by engineer. @@ -3768,6 +5025,11 @@ This is your link for group %@! Moderated at: %@ copied message info + + More + More + swipe action + More improvements are coming soon! More improvements are coming soon! @@ -3778,16 +5040,16 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Most likely this connection is deleted. item status description - - Most likely this contact has deleted the connection with you. - Most likely this contact has deleted the connection with you. - No comment provided by engineer. - Multiple chat profiles Multiple chat profiles @@ -3796,7 +5058,12 @@ This is your link for group %@! Mute Mute - No comment provided by engineer. + notification label action + + + Mute all + Mute all + notification label action Muted when inactive! @@ -3806,7 +5073,7 @@ This is your link for group %@! Name Name - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +5085,26 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + Network decentralization + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Network issues - message expired after many attempts to send it. + snd error text + Network management Network management No comment provided by engineer. + + Network operator + Network operator + No comment provided by engineer. + Network settings Network settings @@ -3833,16 +5115,36 @@ This is your link for group %@! Network status No comment provided by engineer. + + New + New + token status text + New Passcode New Passcode No comment provided by engineer. + + New SOCKS credentials will be used every time you start the app. + New SOCKS credentials will be used every time you start the app. + No comment provided by engineer. + + + New SOCKS credentials will be used for each server. + New SOCKS credentials will be used for each server. + No comment provided by engineer. + New chat New chat No comment provided by engineer. + + New chat experience 🎉 + New chat experience 🎉 + No comment provided by engineer. + New contact request New contact request @@ -3853,11 +5155,6 @@ This is your link for group %@! New contact: notification - - New database archive - New database archive - No comment provided by engineer. - New desktop app! New desktop app! @@ -3868,11 +5165,21 @@ This is your link for group %@! New display name No comment provided by engineer. + + New events + New events + notification + New in %@ New in %@ No comment provided by engineer. + + New media options + New media options + No comment provided by engineer. + New member role New member role @@ -3888,6 +5195,11 @@ This is your link for group %@! New passphrase… No comment provided by engineer. + + New server + New server + No comment provided by engineer. + No No @@ -3898,6 +5210,21 @@ This is your link for group %@! No app password Authentication unavailable + + No chats + No chats + No comment provided by engineer. + + + No chats found + No chats found + No comment provided by engineer. + + + No chats in list %@ + No chats in list %@ + No comment provided by engineer. + No contacts selected No contacts selected @@ -3918,6 +5245,11 @@ This is your link for group %@! No device token! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats No filtered chats @@ -3933,26 +5265,111 @@ This is your link for group %@! No history No comment provided by engineer. + + No info, try to reload + No info, try to reload + No comment provided by engineer. + + + No media & file servers. + No media & file servers. + servers error + + + No message + No message + No comment provided by engineer. + + + No message servers. + No message servers. + servers error + No network connection No network connection No comment provided by engineer. + + No permission to record speech + No permission to record speech + No comment provided by engineer. + + + No permission to record video + No permission to record video + No comment provided by engineer. + No permission to record voice message No permission to record voice message No comment provided by engineer. + + No push server + No push server + No comment provided by engineer. + No received or sent files No received or sent files No comment provided by engineer. + + No servers for private message routing. + No servers for private message routing. + servers error + + + No servers to receive files. + No servers to receive files. + servers error + + + No servers to receive messages. + No servers to receive messages. + servers error + + + No servers to send files. + No servers to send files. + servers error + + + No token! + No token! + alert title + + + No unread chats + No unread chats + No comment provided by engineer. + + + No user identifiers. + No user identifiers. + No comment provided by engineer. + Not compatible! Not compatible! No comment provided by engineer. + + Notes + Notes + No comment provided by engineer. + + + Nothing selected + Nothing selected + No comment provided by engineer. + + + Nothing to forward! + Nothing to forward! + alert title + Notifications Notifications @@ -3963,6 +5380,21 @@ This is your link for group %@! Notifications are disabled! No comment provided by engineer. + + Notifications error + Notifications error + alert title + + + Notifications privacy + Notifications privacy + No comment provided by engineer. + + + Notifications status + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5412,35 @@ This is your link for group %@! Off Off - No comment provided by engineer. + blur media Ok Ok - No comment provided by engineer. + alert button Old database Old database No comment provided by engineer. - - Old database archive - Old database archive - No comment provided by engineer. - One-time invitation link One-time invitation link No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Onion hosts will be required for connection. Requires enabling VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Onion hosts will be **required** for connection. +Requires compatible VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion hosts will be used when available. Requires enabling VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion hosts will be used when available. +Requires compatible VPN. No comment provided by engineer. @@ -4017,9 +5448,19 @@ This is your link for group %@! 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**. - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only chat owners can change preferences. + Only chat owners can change preferences. + No comment provided by engineer. + + + Only client devices store user profiles, contacts, groups, and messages. + Only client devices store user profiles, contacts, groups, and messages. + No comment provided by engineer. + + + Only delete conversation + Only delete conversation No comment provided by engineer. @@ -4037,6 +5478,16 @@ This is your link for group %@! Only group owners can enable voice messages. No comment provided by engineer. + + Only sender and moderators see it + Only sender and moderators see it + No comment provided by engineer. + + + Only you and moderators see it + Only you and moderators see it + No comment provided by engineer. + Only you can add message reactions. Only you can add message reactions. @@ -4090,13 +5541,18 @@ This is your link for group %@! Open Open - No comment provided by engineer. + alert action Open Settings Open Settings No comment provided by engineer. + + Open changes + Open changes + No comment provided by engineer. + Open chat Open chat @@ -4107,31 +5563,46 @@ This is your link for group %@! Open chat console authentication reason + + Open conditions + Open conditions + No comment provided by engineer. + Open group Open group No comment provided by engineer. + + Open link? + Open link? + alert title + Open migration to another device Open migration to another device authentication reason - - Open user profiles - Open user profiles - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Open-source protocol and code – anybody can run the servers. - No comment provided by engineer. - Opening app… Opening app… No comment provided by engineer. + + Operator + Operator + No comment provided by engineer. + + + Operator server + Operator server + alert title + + + Or import archive file + Or import archive file + No comment provided by engineer. + Or paste archive link Or paste archive link @@ -4152,11 +5623,28 @@ This is your link for group %@! Or show this code No comment provided by engineer. + + Or to share privately + Or to share privately + No comment provided by engineer. + + + Organize chats into lists + Organize chats into lists + No comment provided by engineer. + Other Other No comment provided by engineer. + + Other file errors: +%@ + Other file errors: +%@ + alert message + PING count PING count @@ -4192,6 +5680,11 @@ This is your link for group %@! Passcode set! No comment provided by engineer. + + Password + Password + No comment provided by engineer. + Password to show Password to show @@ -4222,14 +5715,14 @@ This is your link for group %@! Paste the link you received No comment provided by engineer. - - People can connect to you only via the links you share. - People can connect to you only via the links you share. + + Pending + Pending No comment provided by engineer. - - Periodically - Periodically + + Periodic + Periodic No comment provided by engineer. @@ -4242,11 +5735,28 @@ This is your link for group %@! Picture-in-picture calls No comment provided by engineer. + + Play from the chat list. + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Please ask your contact to enable calls. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. 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. +Please share any other issues with the developers. + 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. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Please check that you used the correct link or ask your contact to send you another one. @@ -4314,11 +5824,31 @@ Error: %@ Please store passphrase securely, you will NOT be able to change it if you lose it. No comment provided by engineer. + + Please try to disable and re-enable notfications. + Please try to disable and re-enable notfications. + token info + + + Please wait for token activation to complete. + Please wait for token activation to complete. + token info + + + Please wait for token to be registered. + Please wait for token to be registered. + token info + Polish interface Polish interface No comment provided by engineer. + + Port + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Possibly, certificate fingerprint in server address is incorrect @@ -4329,41 +5859,86 @@ Error: %@ Preserve the last message draft, with attachments. No comment provided by engineer. - - Preset server - Preset server - No comment provided by engineer. - Preset server address Preset server address No comment provided by engineer. + + Preset servers + Preset servers + No comment provided by engineer. + Preview Preview No comment provided by engineer. + + Previously connected servers + Previously connected servers + No comment provided by engineer. + Privacy & security Privacy & security No comment provided by engineer. + + Privacy for your customers. + Privacy for your customers. + No comment provided by engineer. + + + Privacy policy and conditions of use. + Privacy policy and conditions of use. + No comment provided by engineer. + Privacy redefined Privacy redefined No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + Private chats, groups and your contacts are not accessible to server operators. + No comment provided by engineer. + Private filenames Private filenames No comment provided by engineer. + + Private media file names. + Private media file names. + No comment provided by engineer. + + + Private message routing + Private message routing + No comment provided by engineer. + + + Private message routing 🚀 + Private message routing 🚀 + No comment provided by engineer. + Private notes Private notes name of notes to self + + Private routing + Private routing + No comment provided by engineer. + + + Private routing error + Private routing error + No comment provided by engineer. + Profile and server connections Profile and server connections @@ -4379,25 +5954,20 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - Profile name - No comment provided by engineer. - - - Profile name: - Profile name: - No comment provided by engineer. - Profile password Profile password No comment provided by engineer. + + Profile theme + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Profile update will be sent to your contacts. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,6 +5989,11 @@ Error: %@ Prohibit messages reactions. No comment provided by engineer. + + Prohibit reporting messages to moderators. + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. Prohibit sending SimpleX links. @@ -4444,11 +6019,23 @@ Error: %@ Prohibit sending voice messages. No comment provided by engineer. + + Protect IP address + Protect IP address + No comment provided by engineer. + Protect app screen Protect app screen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Protect your chat profiles with a password! @@ -4464,6 +6051,21 @@ Error: %@ 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 requires password + No comment provided by engineer. + Push notifications Push notifications @@ -4484,6 +6086,11 @@ Error: %@ Rate the app No comment provided by engineer. + + Reachable chat toolbar + Reachable chat toolbar + No comment provided by engineer. + React… React… @@ -4492,33 +6099,28 @@ Error: %@ Read Read - No comment provided by engineer. + swipe action Read more Read more No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Read more in [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). + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Read more in our GitHub repository. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). @@ -4529,6 +6131,11 @@ Error: %@ Receipts are disabled No comment provided by engineer. + + Receive errors + Receive errors + No comment provided by engineer. + Received at Received at @@ -4549,16 +6156,26 @@ Error: %@ Received message message info title + + Received messages + Received messages + No comment provided by engineer. + + + Received reply + Received reply + No comment provided by engineer. + + + Received total + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Receiving address will be changed to a different server. Address change will complete after sender comes online. No comment provided by engineer. - - Receiving concurrency - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. Receiving file will be stopped. @@ -4584,11 +6201,36 @@ Error: %@ Recipients see updates as you type them. No comment provided by engineer. + + Reconnect + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Reconnect all connected servers to force message delivery. It uses additional traffic. No comment provided by engineer. + + Reconnect all servers + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + Reconnect server? + No comment provided by engineer. + Reconnect servers? Reconnect servers? @@ -4609,10 +6251,26 @@ Error: %@ Reduced battery usage No comment provided by engineer. + + Register + Register + No comment provided by engineer. + + + Register notification token? + Register notification token? + token info + + + Registered + Registered + token status text + Reject Reject - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4639,6 +6297,16 @@ Error: %@ Remove No comment provided by engineer. + + Remove archive? + Remove archive? + No comment provided by engineer. + + + Remove image + Remove image + No comment provided by engineer. + Remove member Remove member @@ -4699,6 +6367,56 @@ Error: %@ Reply chat item action + + Report + Report + chat item action + + + Report content: only group moderators will see it. + Report content: only group moderators will see it. + report reason + + + Report member profile: only group moderators will see it. + Report member profile: only group moderators will see it. + report reason + + + Report other: only group moderators will see it. + Report other: only group moderators will see it. + report reason + + + Report reason? + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + Report violation: only group moderators will see it. + report reason + + + Report: %@ + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + Reports + No comment provided by engineer. + Required Required @@ -4709,16 +6427,41 @@ Error: %@ Reset No comment provided by engineer. + + Reset all hints + Reset all hints + No comment provided by engineer. + + + Reset all statistics + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + Reset all statistics? + No comment provided by engineer. + Reset colors Reset colors No comment provided by engineer. + + Reset to app theme + Reset to app theme + No comment provided by engineer. + Reset to defaults Reset to defaults No comment provided by engineer. + + Reset to user theme + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Restart the app to create a new chat profile @@ -4759,9 +6502,9 @@ Error: %@ Reveal chat item action - - Revert - Revert + + Review conditions + Review conditions No comment provided by engineer. @@ -4789,9 +6532,19 @@ Error: %@ Run chat No comment provided by engineer. - - SMP servers - SMP servers + + SMP server + SMP server + No comment provided by engineer. + + + SOCKS proxy + SOCKS proxy + No comment provided by engineer. + + + Safely receive files + Safely receive files No comment provided by engineer. @@ -4802,43 +6555,44 @@ Error: %@ Save Save - chat item action + alert button +chat item action Save (and notify contacts) Save (and notify contacts) - No comment provided by engineer. + alert button Save and notify contact Save and notify contact - No comment provided by engineer. + alert button Save and notify group members Save and notify group members No comment provided by engineer. + + Save and reconnect + Save and reconnect + No comment provided by engineer. + Save and update group profile Save and update group profile No comment provided by engineer. - - Save archive - Save archive - No comment provided by engineer. - - - Save auto-accept settings - Save auto-accept settings - No comment provided by engineer. - Save group profile Save group profile No comment provided by engineer. + + Save list + Save list + No comment provided by engineer. + Save passphrase and open chat Save passphrase and open chat @@ -4852,7 +6606,7 @@ Error: %@ Save preferences? Save preferences? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6621,18 @@ Error: %@ Save servers? Save servers? - No comment provided by engineer. - - - Save settings? - Save settings? - No comment provided by engineer. + alert title Save welcome message? Save welcome message? No comment provided by engineer. + + Save your profile? + Save your profile? + alert title + Saved Saved @@ -4899,6 +6653,21 @@ Error: %@ Saved message message info title + + Saving %lld messages + Saving %lld messages + No comment provided by engineer. + + + Scale + Scale + No comment provided by engineer. + + + Scan / Paste link + Scan / Paste link + No comment provided by engineer. + Scan QR code Scan QR code @@ -4939,11 +6708,21 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + Secondary + No comment provided by engineer. + Secure queue Secure queue server test step + + Secured + Secured + No comment provided by engineer. + Security assessment Security assessment @@ -4957,6 +6736,21 @@ Error: %@ Select Select + chat item action + + + Select chat profile + Select chat profile + No comment provided by engineer. + + + Selected %lld + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4994,11 +6788,6 @@ Error: %@ 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 Send direct message to connect @@ -5009,6 +6798,11 @@ Error: %@ Send disappearing message No comment provided by engineer. + + Send errors + Send errors + No comment provided by engineer. + Send link previews Send link previews @@ -5019,14 +6813,29 @@ Error: %@ Send live message No comment provided by engineer. + + Send message to enable calls. + 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. + Send messages directly when IP address is protected and your or destination server does not support private routing. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Send messages directly when your or destination server does not support private routing. + No comment provided by engineer. + Send notifications Send notifications No comment provided by engineer. - - Send notifications: - Send notifications: + + Send private reports + Send private reports No comment provided by engineer. @@ -5052,7 +6861,7 @@ Error: %@ Sender cancelled file transfer. Sender cancelled file transfer. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6918,11 @@ Error: %@ Sent at: %@ copied message info + + Sent directly + Sent directly + No comment provided by engineer. + Sent file event Sent file event @@ -5119,11 +6933,71 @@ Error: %@ Sent message message info title + + Sent messages + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. Sent messages will be deleted after set time. No comment provided by engineer. + + Sent reply + Sent reply + No comment provided by engineer. + + + Sent total + Sent total + No comment provided by engineer. + + + Sent via proxy + Sent via proxy + No comment provided by engineer. + + + Server + Server + No comment provided by engineer. + + + Server added to operator %@. + Server added to operator %@. + alert message + + + Server address + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + Server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Server operator changed. + Server operator changed. + alert title + + + Server operators + Server operators + No comment provided by engineer. + + + Server protocol changed. + Server protocol changed. + alert title + Server requires authorization to create queues, check password Server requires authorization to create queues, check password @@ -5139,11 +7013,36 @@ Error: %@ Server test failed! No comment provided by engineer. + + Server type + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers Servers No comment provided by engineer. + + Servers info + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code Session code @@ -5154,11 +7053,21 @@ Error: %@ Set 1 day No comment provided by engineer. + + Set chat name… + Set chat name… + No comment provided by engineer. + Set contact name… Set contact name… No comment provided by engineer. + + Set default theme + Set default theme + No comment provided by engineer. + Set group preferences Set group preferences @@ -5169,6 +7078,11 @@ Error: %@ Set it instead of system authentication. No comment provided by engineer. + + Set message expiration in chats. + Set message expiration in chats. + No comment provided by engineer. + Set passcode Set passcode @@ -5199,6 +7113,11 @@ Error: %@ Settings No comment provided by engineer. + + Settings were changed. + Settings were changed. + alert message + Shape profile images Shape profile images @@ -5207,21 +7126,42 @@ Error: %@ Share Share - chat item action + alert action +chat item action Share 1-time link Share 1-time link No comment provided by engineer. + + Share 1-time link with a friend + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + Share SimpleX address on social media. + No comment provided by engineer. + Share address Share address No comment provided by engineer. + + Share address publicly + Share address publicly + No comment provided by engineer. + Share address with contacts? Share address with contacts? + alert title + + + Share from other apps. + Share from other apps. No comment provided by engineer. @@ -5229,16 +7169,31 @@ Error: %@ Share link No comment provided by engineer. + + Share profile + Share profile + No comment provided by engineer. + Share this 1-time invite link Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + Share to SimpleX + No comment provided by engineer. + Share with contacts Share with contacts No comment provided by engineer. + + Short link + Short link + No comment provided by engineer. + Show QR code Show QR code @@ -5259,21 +7214,46 @@ Error: %@ Show last messages No comment provided by engineer. + + Show message status + Show message status + No comment provided by engineer. + + + Show percentage + Show percentage + No comment provided by engineer. + Show preview Show preview No comment provided by engineer. + + Show → on messages sent via private routing. + Show → on messages sent via private routing. + No comment provided by engineer. + Show: Show: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Address No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. SimpleX Chat security was audited by Trail of Bits. @@ -5304,6 +7284,21 @@ Error: %@ SimpleX address No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + SimpleX address or 1-time link? + No comment provided by engineer. + + + SimpleX channel link + SimpleX channel link + simplex link type + SimpleX contact address SimpleX contact address @@ -5324,9 +7319,9 @@ Error: %@ SimpleX links chat feature - - SimpleX links are prohibited in this group. - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. + SimpleX links are prohibited. No comment provided by engineer. @@ -5339,11 +7334,21 @@ Error: %@ SimpleX one-time invitation simplex link type + + SimpleX protocols reviewed by Trail of Bits. + SimpleX protocols reviewed by Trail of Bits. + No comment provided by engineer. + Simplified incognito mode Simplified incognito mode No comment provided by engineer. + + Size + Size + No comment provided by engineer. + Skip Skip @@ -5359,16 +7364,49 @@ Error: %@ Small groups (max 20) No comment provided by engineer. + + Soft + Soft + blur media + + + Some app settings were not migrated. + Some app settings were not migrated. + No comment provided by engineer. + + + Some file(s) were not exported: + 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. 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: + Some non-fatal errors occurred during import: + No comment provided by engineer. + + + Some servers failed the test: +%@ + Some servers failed the test: +%@ + alert message + Somebody Somebody notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Square, circle, or anything in between. @@ -5389,6 +7427,16 @@ Error: %@ Start migration No comment provided by engineer. + + Starting from %@. + Starting from %@. + No comment provided by engineer. + + + Statistics + Statistics + No comment provided by engineer. + Stop Stop @@ -5404,11 +7452,6 @@ Error: %@ Stop chat No comment provided by engineer. - - Stop chat to enable database actions - 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. Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. @@ -5437,28 +7480,63 @@ Error: %@ Stop sharing Stop sharing - No comment provided by engineer. + alert action Stop sharing address? Stop sharing address? - No comment provided by engineer. + alert title Stopping chat Stopping chat No comment provided by engineer. + + Storage + Storage + No comment provided by engineer. + + + Strong + Strong + blur media + Submit Submit No comment provided by engineer. + + Subscribed + Subscribed + No comment provided by engineer. + + + Subscription errors + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Support SimpleX Chat No comment provided by engineer. + + Switch audio and video during the call. + Switch audio and video during the call. + No comment provided by engineer. + + + Switch chat profile for 1-time invitations. + Switch chat profile for 1-time invitations. + No comment provided by engineer. + System System @@ -5469,11 +7547,21 @@ Error: %@ System authentication No comment provided by engineer. + + TCP connection + TCP connection + No comment provided by engineer. + TCP connection timeout TCP connection timeout No comment provided by engineer. + + TCP port for messaging + TCP port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5489,11 +7577,21 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Tail + No comment provided by engineer. + Take picture Take picture No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Tap button @@ -5529,16 +7627,21 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - Tap to start a new chat - No comment provided by engineer. + + Temporary file error + Temporary file error + file error alert title Test failed at step %@. Test failed at step %@. server test failure + + Test notifications + Test notifications + No comment provided by engineer. + Test server Test server @@ -5552,7 +7655,7 @@ Error: %@ Tests failed! Tests failed! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7672,6 @@ Error: %@ Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - The 1st platform without any user identifiers – private by design. - No comment provided by engineer. - 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. @@ -5586,6 +7684,16 @@ It can happen because of some bug or when the connection is compromised.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. + 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). + The app will ask to confirm downloads from unknown file servers (except .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. The attempt to change database passphrase was not completed. @@ -5596,6 +7704,11 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + 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! The connection you accepted will be cancelled! @@ -5616,6 +7729,11 @@ It can happen because of some bug or when the connection is compromised.The encryption is working and the new encryption agreement is not required. It may result in connection errors! No comment provided by engineer. + + The future of messaging + The future of messaging + No comment provided by engineer. + The hash of the previous message is different. The hash of the previous message is different. @@ -5631,9 +7749,14 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging - The next generation of private messaging + + The messages will be deleted for all members. + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + The messages will be marked as moderated for all members. No comment provided by engineer. @@ -5641,9 +7764,14 @@ It can happen because of some bug or when the connection is compromised.The old database was not removed during the migration, it can be deleted. No comment provided by engineer. - - The profile is only shared with your contacts. - The profile is only shared with your contacts. + + The same conditions will apply to operator **%@**. + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! + The second preset operator in the app! No comment provided by engineer. @@ -5661,14 +7789,29 @@ It can happen because of some bug or when the connection is compromised.The servers for new connections of your current chat profile **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - Theme + + The uploaded database archive will be permanently removed from the servers. + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + + + Themes + Themes + No comment provided by engineer. + + + These conditions will also apply for: **%@**. + These conditions will also apply for: **%@**. No comment provided by engineer. @@ -5691,6 +7834,11 @@ It can happen because of some bug or when the connection is compromised.This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. @@ -5736,11 +7884,31 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! 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. + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + No comment provided by engineer. + + + This link was used with another mobile device, please create a new link on the desktop. + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + + + This message was deleted or not received yet. + This message was deleted or not received yet. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. This setting applies to messages in your current chat profile **%@**. No comment provided by engineer. + + Title + Title + No comment provided by engineer. + To ask any questions and to receive updates: To ask any questions and to receive updates: @@ -5761,9 +7929,9 @@ It can happen because of some bug or when the connection is compromised.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. - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect against your link being replaced, you can compare contact security codes. + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -5771,6 +7939,11 @@ It can happen because of some bug or when the connection is compromised.To protect timezone, image/voice files use UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,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. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + No comment provided by engineer. + + + To receive + To receive + No comment provided by engineer. + + + To record speech please grant permission to use Microphone. + To record speech please grant permission to use Microphone. + No comment provided by engineer. + + + To record video please grant permission to use Camera. + To record video please grant permission to use Camera. + No comment provided by engineer. + To record voice message please grant permission to use Microphone. To record voice message please grant permission to use Microphone. @@ -5788,26 +7981,61 @@ You will be prompted to complete authentication before this feature is enabled.< 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 + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. 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. + 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. To verify end-to-end encryption with your contact compare (or scan) the code on your devices. No comment provided by engineer. + + Toggle chat list: + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. Toggle incognito when connecting. No comment provided by engineer. + + Token status: %@. + Token status: %@. + token status + + + Toolbar opacity + Toolbar opacity + No comment provided by engineer. + + + Total + Total + No comment provided by engineer. + Transport isolation Transport isolation No comment provided by engineer. + + Transport sessions + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Trying to connect to the server used to receive messages from this contact (error: %@). @@ -5863,10 +8091,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. - - Unexpected error: %@ - Unexpected error: %@ - item status description + + Undelivered messages + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5876,7 +8104,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. Unfav. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +8141,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unknown error No comment provided by engineer. + + Unknown servers! + Unknown servers! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. @@ -5948,11 +8181,16 @@ To connect, please ask your contact to create another connection link and check Unmute Unmute - No comment provided by engineer. + notification label action Unread Unread + swipe action + + + Unsupported connection link + Unsupported connection link No comment provided by engineer. @@ -5965,11 +8203,6 @@ To connect, please ask your contact to create another connection link and check Update No comment provided by engineer. - - Update .onion hosts setting? - Update .onion hosts setting? - No comment provided by engineer. - Update database passphrase Update database passphrase @@ -5980,9 +8213,14 @@ To connect, please ask your contact to create another connection link and check Update network settings? No comment provided by engineer. - - Update transport isolation mode? - Update transport isolation mode? + + Update settings? + Update settings? + No comment provided by engineer. + + + Updated conditions + Updated conditions No comment provided by engineer. @@ -5990,16 +8228,16 @@ To connect, please ask your contact to create another connection link and check 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. - Updating this setting will re-connect the client to all servers. - No comment provided by engineer. - Upgrade and open chat Upgrade and open chat No comment provided by engineer. + + Upload errors + Upload errors + No comment provided by engineer. + Upload failed Upload failed @@ -6010,21 +8248,51 @@ To connect, please ask your contact to create another connection link and check Upload file server test step + + Uploaded + Uploaded + No comment provided by engineer. + + + Uploaded files + Uploaded files + No comment provided by engineer. + Uploading archive Uploading archive No comment provided by engineer. + + Use %@ + Use %@ + No comment provided by engineer. + Use .onion hosts Use .onion hosts No comment provided by engineer. + + Use SOCKS proxy + Use SOCKS proxy + No comment provided by engineer. + Use SimpleX Chat servers? Use SimpleX Chat servers? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Use chat @@ -6035,6 +8303,16 @@ To connect, please ask your contact to create another connection link and check Use current profile No comment provided by engineer. + + Use for files + Use for files + No comment provided by engineer. + + + Use for messages + Use for messages + No comment provided by engineer. + Use for new connections Use for new connections @@ -6060,24 +8338,54 @@ To connect, please ask your contact to create another connection link and check Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + Use private routing with unknown servers. + No comment provided by engineer. + Use server Use server No comment provided by engineer. + + Use servers + Use servers + No comment provided by engineer. + + + Use short links (BETA) + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Use the app while in the call. No comment provided by engineer. - - User profile - User profile + + Use the app with one hand. + Use the app with one hand. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Using .onion hosts requires compatible VPN provider. + + Use web port + Use web port + No comment provided by engineer. + + + User selection + User selection + No comment provided by engineer. + + + Username + Username No comment provided by engineer. @@ -6150,11 +8458,21 @@ To connect, please ask your contact to create another connection link and check Videos and files up to 1gb No comment provided by engineer. + + View conditions + View conditions + No comment provided by engineer. + View security code View security code No comment provided by engineer. + + View updated conditions + View updated conditions + No comment provided by engineer. + Visible history Visible history @@ -6170,9 +8488,9 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. - Voice messages are prohibited in this group. + + Voice messages are prohibited. + Voice messages are prohibited. No comment provided by engineer. @@ -6210,6 +8528,16 @@ To connect, please ask your contact to create another connection link and check Waiting for video No comment provided by engineer. + + Wallpaper accent + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Warning: starting chat on multiple devices is not supported and will cause message delivery failures @@ -6255,9 +8583,9 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - When people request to connect, you can accept or reject it. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -6295,24 +8623,39 @@ To connect, please ask your contact to create another connection link and check With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + alert message + Wrong database passphrase Wrong database passphrase No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Wrong passphrase! No comment provided by engineer. - - XFTP servers - XFTP servers - No comment provided by engineer. - - - You - You + + XFTP server + XFTP server No comment provided by engineer. @@ -6340,6 +8683,11 @@ To connect, please ask your contact to create another connection link and check You are already connected to %@. No comment provided by engineer. + + You are already connected with %@. + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. You are already connecting to %@. @@ -6387,11 +8735,26 @@ Repeat join request? 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. + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. You can accept calls from lock screen, without device and app authentication. No comment provided by engineer. + + You can change it in Appearance settings. + You can change it in Appearance settings. + No comment provided by engineer. + + + You can configure servers via settings. + You can configure servers via settings. + No comment provided by engineer. + You can create it later You can create it later @@ -6422,11 +8785,21 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ - You can now send messages to %@ + + You can now chat with %@ + You can now chat with %@ notification body + + You can send messages to %@ from Archived contacts. + 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. + 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. You can set lock screen notification preview via settings. @@ -6442,16 +8815,16 @@ Repeat join request? 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 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 You can start chat via app Settings / Database or by restarting the app No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. You can turn on SimpleX Lock via Settings. @@ -6465,23 +8838,23 @@ Repeat join request? You can view invitation link again in connection details. You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! You can't send messages! 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 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. You could not be verified; please try again. No comment provided by engineer. + + You decide who can connect. + You decide who can connect. + No comment provided by engineer. + You have already requested connection via this address! You have already requested connection via this address! @@ -6494,11 +8867,6 @@ Repeat connection request? Repeat connection request? No comment provided by engineer. - - You have no chats - 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. You have to enter passphrase every time the app starts - it is not stored on the device. @@ -6519,11 +8887,26 @@ Repeat connection request? You joined this group. Connecting to inviting group member. No comment provided by engineer. + + You may migrate the exported database. + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + 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. You need to allow your contact to send voice messages to be able to send them. @@ -6539,6 +8922,11 @@ Repeat connection request? You sent group invitation No comment provided by engineer. + + You should receive notifications. + You should receive notifications. + token info + You will be connected to group when the group host's device is online, please wait or check later! You will be connected to group when the group host's device is online, please wait or check later! @@ -6574,6 +8962,11 @@ Repeat connection request? You will still receive calls and notifications from muted profiles when they are active. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + 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. You will stop receiving messages from this group. Chat history will be preserved. @@ -6594,31 +8987,16 @@ Repeat connection request? 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 - Your %@ servers - No comment provided by engineer. - Your ICE servers Your ICE servers No comment provided by engineer. - - Your SMP servers - Your SMP servers - No comment provided by engineer. - Your SimpleX address Your SimpleX address No comment provided by engineer. - - Your XFTP servers - Your XFTP servers - No comment provided by engineer. - Your calls Your calls @@ -6634,16 +9012,19 @@ Repeat connection request? Your chat database is not encrypted - set passphrase to encrypt it. No comment provided by engineer. + + Your chat preferences + Your chat preferences + alert title + Your chat profiles Your chat profiles No comment provided by engineer. - - 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). - 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). + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. No comment provided by engineer. @@ -6661,6 +9042,11 @@ You can cancel this connection and remove the contact (and try later with a new Your contacts will remain connected. No comment provided by engineer. + + Your credentials may be sent unencrypted. + Your credentials may be sent unencrypted. + 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. @@ -6691,33 +9077,36 @@ You can cancel this connection and remove the contact (and try later with a new Your profile **%@** will be shared. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. + + Your profile is stored on your device and only shared with your contacts. + Your profile is stored on your device and only shared with your contacts. No comment provided by engineer. - - Your profile, contacts and delivered messages are stored on your device. - Your profile, contacts and delivered messages are stored on your device. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your random profile Your random profile No comment provided by engineer. - - Your server - Your server - No comment provided by engineer. - Your server address Your server address No comment provided by engineer. + + Your servers + Your servers + No comment provided by engineer. + Your settings Your settings @@ -6758,6 +9147,11 @@ SimpleX servers cannot see your profile. accepted call call status + + accepted invitation + accepted invitation + chat list item title + admin admin @@ -6793,6 +9187,16 @@ SimpleX servers cannot see your profile. and %lld other events No comment provided by engineer. + + archived report + archived report + No comment provided by engineer. + + + attempts + attempts + No comment provided by engineer. + audio call (not e2e encrypted) audio call (not e2e encrypted) @@ -6826,13 +9230,19 @@ SimpleX servers cannot see your profile. blocked by admin blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold bold No comment provided by engineer. + + call + call + No comment provided by engineer. + call error call error @@ -6936,7 +9346,7 @@ SimpleX servers cannot see your profile. connecting… connecting… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +9393,16 @@ SimpleX servers cannot see your profile. days time unit + + decryption errors + decryption errors + No comment provided by engineer. + default (%@) default (%@) - pref value + delete after time +pref value default (no) @@ -7033,6 +9449,11 @@ SimpleX servers cannot see your profile. duplicate message integrity error chat item + + duplicates + duplicates + No comment provided by engineer. + e2e encrypted e2e encrypted @@ -7108,9 +9529,9 @@ SimpleX servers cannot see your profile. error No comment provided by engineer. - - event happened - event happened + + expired + expired No comment provided by engineer. @@ -7143,6 +9564,11 @@ SimpleX servers cannot see your profile. iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. No comment provided by engineer. + + inactive + inactive + No comment provided by engineer. + incognito via contact address link incognito via contact address link @@ -7183,6 +9609,11 @@ SimpleX servers cannot see your profile. invitation to group %@ group name + + invite + invite + No comment provided by engineer. + invited invited @@ -7238,6 +9669,11 @@ SimpleX servers cannot see your profile. connected rcv group event chat item + + message + message + No comment provided by engineer. + message received message received @@ -7263,6 +9699,11 @@ SimpleX servers cannot see your profile. moderated by %@ marked deleted chat item preview text + + moderator + moderator + member role + months months @@ -7271,7 +9712,7 @@ SimpleX servers cannot see your profile. never never - No comment provided by engineer. + delete after time new message @@ -7302,8 +9743,8 @@ SimpleX servers cannot see your profile. off off enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9761,16 @@ SimpleX servers cannot see your profile. on group pref value + + other + other + No comment provided by engineer. + + + other errors + other errors + No comment provided by engineer. + owner owner @@ -7335,6 +9786,16 @@ SimpleX servers cannot see your profile. peer-to-peer No comment provided by engineer. + + pending + pending + No comment provided by engineer. + + + pending approval + pending approval + No comment provided by engineer. + quantum resistant e2e encryption quantum resistant e2e encryption @@ -7350,6 +9811,11 @@ SimpleX servers cannot see your profile. received confirmation… No comment provided by engineer. + + rejected + rejected + No comment provided by engineer. + rejected call rejected call @@ -7380,6 +9846,11 @@ SimpleX servers cannot see your profile. removed you rcv group event chat item + + requested to connect + requested to connect + chat list item title + saved saved @@ -7390,6 +9861,11 @@ SimpleX servers cannot see your profile. saved from %@ No comment provided by engineer. + + search + search + No comment provided by engineer. + sec sec @@ -7415,6 +9891,15 @@ SimpleX servers cannot see your profile. send direct message No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address set new contact address @@ -7455,11 +9940,21 @@ SimpleX servers cannot see your profile. unknown connection info + + unknown servers + unknown servers + No comment provided by engineer. + unknown status unknown status No comment provided by engineer. + + unprotected + unprotected + No comment provided by engineer. + updated group profile updated group profile @@ -7500,6 +9995,11 @@ SimpleX servers cannot see your profile. via relay No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) video call (not e2e encrypted) @@ -7525,6 +10025,11 @@ SimpleX servers cannot see your profile. weeks time unit + + when IP hidden + when IP hidden + No comment provided by engineer. + yes yes @@ -7609,7 +10114,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7646,7 +10151,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7666,4 +10171,250 @@ SimpleX servers cannot see your profile.
+ +
+ +
+ + + %d new events + %d new events + notification body + + + From %d chat(s) + From %d chat(s) + notification body + + + From: %@ + From: %@ + notification body + + + New events + New events + notification + + + New messages + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + App is locked! + No comment provided by engineer. + + + Cancel + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + Cannot forward message + No comment provided by engineer. + + + Comment + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + Database encrypted! + No comment provided by engineer. + + + Database error + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + Database upgrade required + No comment provided by engineer. + + + Error preparing file + Error preparing file + No comment provided by engineer. + + + Error preparing message + Error preparing message + No comment provided by engineer. + + + Error: %@ + Error: %@ + No comment provided by engineer. + + + File error + File error + No comment provided by engineer. + + + Incompatible database version + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + Keychain error + No comment provided by engineer. + + + Large file! + Large file! + No comment provided by engineer. + + + No active profile + No active profile + No comment provided by engineer. + + + Ok + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + Sending message… + No comment provided by engineer. + + + Share + Share + No comment provided by engineer. + + + Slow network? + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + Unsupported format + No comment provided by engineer. + + + Wait + Wait + No comment provided by engineer. + + + Wrong database passphrase + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
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 5d2482ae27..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 @@
- +
- - - - - - 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) @@ -127,9 +100,14 @@ %@ está verificado No comment provided by engineer. + + %@ server + %@ servidor + No comment provided by engineer. + %@ servers - Servidores %@ + %@ servidores No comment provided by engineer. @@ -142,6 +120,11 @@ ¡ %@ quiere contactar! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ y %lld miembro(s) más @@ -159,37 +142,67 @@ %d days - %d días + %d día(s) time interval + + %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. + forward confirmation reason + %d hours - %d horas + %d hora(s) time interval + + %d messages not forwarded + %d mensaje(s) no enviado(s) + alert title + %d min - %d minutos + %d minuto(s) time interval %d months - %d meses + %d mes(es) time interval %d sec - %d segundos + %d segundo(s) time interval + + %d seconds(s) + %d segundos + delete after time + %d skipped message(s) - %d mensaje(s) saltado(s + %d mensaje(s) omitido(s) integrity error chat item %d weeks - %d semanas + %d semana(s) time interval @@ -252,11 +265,6 @@ %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 @@ -307,11 +315,6 @@ %u mensaje(s) omitido(s). No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nuevo) @@ -322,19 +325,9 @@ (este dispositivo v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Añadir contacto**: crea un enlace de invitación nuevo o usa un enlace recibido. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link 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. @@ -342,13 +335,13 @@ **Crear grupo**: crea un grupo nuevo. 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. + + **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). + + **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. @@ -359,14 +352,19 @@ **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. + **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. + + **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 / 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. @@ -392,11 +390,6 @@ \*bold* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +426,6 @@ - historial de edición. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 seg @@ -451,7 +439,8 @@ 1 day un dia - time interval + delete after time +time interval 1 hour @@ -466,12 +455,29 @@ 1 month un mes - time interval + delete after time +time interval 1 week una semana - time interval + delete after time +time interval + + + 1 year + 1 año + delete after time + + + 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 @@ -488,11 +494,6 @@ 30 segundos No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -512,7 +513,7 @@ A new random profile will be shared. - Se compartirá un perfil nuevo aleatorio. + Compartirás un perfil nuevo aleatorio. No comment provided by engineer. @@ -524,7 +525,7 @@ 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. 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. +**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. @@ -542,23 +543,18 @@ ¿Cancelar el cambio de servidor? No comment provided by engineer. - - About SimpleX - Acerca de SimpleX - No comment provided by engineer. - About SimpleX Chat Sobre SimpleX Chat 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. - - Accent color + + Accent Color No comment provided by engineer. @@ -566,7 +562,13 @@ Accept Aceptar accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Aceptar condiciones + No comment provided by engineer. Accept connection request? @@ -581,21 +583,47 @@ Accept incognito Aceptar incógnito - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Condiciones aceptadas + No comment provided by engineer. + + + Acknowledged + Confirmaciones + No comment provided by engineer. + + + Acknowledgement errors + Errores de confirmación + No comment provided by engineer. + + + Active + Activo + token status text + + + 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 contact - Añadir contacto + + Add friends + Añadir amigos No comment provided by engineer. - - Add preset servers - Añadir servidores predefinidos + + Add list + Añadir lista No comment provided by engineer. @@ -603,14 +631,19 @@ Añadir perfil No comment provided by engineer. + + 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 server… - Añadir servidor… + + Add team members + Añadir miembros del equipo No comment provided by engineer. @@ -618,11 +651,46 @@ 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 @@ -633,6 +701,16 @@ 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 + No comment provided by engineer. + Admins can block a member for all. Los administradores pueden bloquear a un miembro para los demás. @@ -648,6 +726,16 @@ Configuración avanzada de red No comment provided by engineer. + + Advanced settings + Configuración avanzada + 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. @@ -655,27 +743,42 @@ All chats and messages will be deleted - this cannot be undone! - Se eliminarán todos los chats y mensajes. ¡No podrá deshacerse! + Se eliminarán todos los chats y mensajes. ¡No puede deshacerse! No comment provided by engineer. + + 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. + alert message + All data is erased when it is entered. Al introducirlo todos los datos son eliminados. No comment provided by engineer. + + 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 group members will remain connected. Todos los miembros del grupo permanecerán conectados. 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 borrados. ¡No podrá deshacerse! + 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 podrá deshacerse! + Se eliminarán todos los mensajes SOLO para tí. ¡No puede deshacerse! No comment provided by engineer. @@ -683,6 +786,21 @@ ¡Los mensajes nuevos de %@ estarán ocultos! No comment provided by engineer. + + All profiles + Todos los perfiles + profile dropdown + + + 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. @@ -708,11 +826,21 @@ 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 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) @@ -738,14 +866,24 @@ Permites el envío de mensajes temporales. No comment provided by engineer. + + 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 SimpleX links. - Permitir enviar enlaces SimpleX. + Se permite enviar enlaces SimpleX. No comment provided by engineer. @@ -808,6 +946,11 @@ ¡Ya en proceso de unirte al grupo! No comment provided by engineer. + + Always use private routing. + Usar siempre enrutamiento privado. + No comment provided by engineer. + Always use relay Usar siempre retransmisor @@ -818,11 +961,21 @@ Se creará un perfil vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre. No comment provided by engineer. + + Another reason + Otro motivo + report reason + 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: %@ @@ -838,9 +991,14 @@ Cifrado de los nuevos archivos locales (excepto vídeos). No comment provided by engineer. + + App group: + Grupo app: + No comment provided by engineer. + App icon - Icono aplicación + Icono de la aplicación No comment provided by engineer. @@ -853,6 +1011,11 @@ 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 @@ -873,11 +1036,56 @@ 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? + No comment provided by engineer. + + + Archive reports + Archivar informes + swipe action + + + Archived contacts + Contactos archivados + No comment provided by engineer. + Archiving database Archivando base de datos @@ -943,11 +1151,21 @@ Aceptar imágenes automáticamente No comment provided by engineer. + + Auto-accept settings + Auto aceptar configuración + alert title + Back Volver No comment provided by engineer. + + Background + Fondo + No comment provided by engineer. + Bad desktop address Dirección ordenador incorrecta @@ -963,16 +1181,61 @@ Hash 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 @@ -1008,6 +1271,16 @@ 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. + Both you and your contact can add message reactions. Tanto tú como tu contacto podéis añadir reacciones a los mensajes. @@ -1038,9 +1311,33 @@ 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. + + 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 (por defecto) o [por conexión](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: +- send only legal content in public groups. +- respect other users – no spam. + Al usar SimpleX Chat, aceptas: +- enviar únicamente contenido legal en los grupos públicos. +- respetar a los demás usuarios – spam prohibido. No comment provided by engineer. @@ -1053,11 +1350,26 @@ Llamadas No comment provided by engineer. + + 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! @@ -1068,10 +1380,16 @@ ¡No se pueden invitar contactos! No comment provided by engineer. + + Can't message member + No se pueden enviar mensajes al miembro + No comment provided by engineer. + Cancel Cancelar - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1401,20 @@ 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. + Cannot receive file No se puede recibir el archivo - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Capacidad excedida - el destinatario no ha recibido los mensajes previos. + snd error text Cellular @@ -1098,6 +1426,16 @@ Cambiar No comment provided by engineer. + + Change automatic message deletion? + ¿Modificar la eliminación automática de mensajes? + alert title + + + Change chat profiles + Cambiar perfil de usuario + authentication reason + Change database passphrase? ¿Cambiar contraseña de la base de datos? @@ -1142,11 +1480,26 @@ Change self-destruct passcode Cambiar código autodestrucción authentication reason - set passcode view +set passcode view - - 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. @@ -1156,7 +1509,7 @@ Chat database - Base de datos del chat + Base de datos de SimpleX No comment provided by engineer. @@ -1164,6 +1517,11 @@ 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 @@ -1171,17 +1529,22 @@ Chat is running - Chat está en ejecución + SimpleX está en ejecución No comment provided by engineer. Chat is stopped - Chat está parado + 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. - Chat parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar 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. @@ -1194,15 +1557,50 @@ Preferencias de Chat No comment provided by engineer. + + Chat preferences were changed. + Las preferencias del chat han sido modificadas. + alert message + + + 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. + No comment provided by engineer. + Check server address and try again. Comprueba la dirección del servidor e inténtalo de nuevo. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1622,25 @@ 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 + No comment provided by engineer. + Clear Vaciar - No comment provided by engineer. + swipe action Clear conversation @@ -1239,9 +1652,19 @@ ¿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? - ¿Borrar notas privadas? + ¿Eliminar notas privadas? No comment provided by engineer. @@ -1249,11 +1672,21 @@ Eliminar verificación No comment provided by engineer. - - Colors - Colores + + 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. + + + Community guidelines violation + Violación de las normas de la comunidad + report reason + Compare file Comparar archivo @@ -1264,11 +1697,56 @@ Compara los códigos de seguridad con tus contactos. 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 @@ -1279,11 +1757,21 @@ Confirma Código 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 @@ -1301,7 +1789,7 @@ Confirm that you remember database passphrase to migrate it. - Confirma que recuerdas la frase de contraseña de la base de datos para migrarla. + Para migrar la base de datos confirma que recuerdas la frase de contraseña. No comment provided by engineer. @@ -1309,6 +1797,11 @@ Confirmar subida No comment provided by engineer. + + Confirmed + Confirmado + token status text + Connect Conectar @@ -1329,6 +1822,11 @@ Conectar con ordenador 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? @@ -1368,16 +1866,31 @@ This is your own one-time link! Conectar con %@ No comment provided by engineer. + + Connected + Conectadas + No comment provided by engineer. + Connected desktop Ordenador conectado No comment provided by engineer. + + 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 to server… Conectando con el servidor… @@ -1388,6 +1901,11 @@ This is your own one-time link! Conectando con el servidor... (error: %@) 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 @@ -1398,6 +1916,16 @@ This is your own one-time link! 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 @@ -1405,7 +1933,24 @@ This is your own one-time link! Connection error (AUTH) - Error conexión (Autenticación) + Error de conexión (Autenticación) + No comment provided by engineer. + + + Connection is blocked by server operator: +%@ + Conexión bloqueada por el operador del servidor: +%@ + 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. @@ -1413,6 +1958,16 @@ This is your own one-time link! ¡Solicitud de conexión enviada! No comment provided by engineer. + + 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 @@ -1420,7 +1975,17 @@ This is your own one-time link! Connection timeout - Tiempo de conexión expirado + Tiempo de conexión agotado + No comment provided by engineer. + + + Connection with desktop stopped + La conexión con el escritorio (desktop) se ha parado + No comment provided by engineer. + + + Connections + Conexiones No comment provided by engineer. @@ -1433,6 +1998,11 @@ This is your own one-time link! El contácto ya existe No comment provided by engineer. + + Contact deleted! + ¡Contacto eliminado! + No comment provided by engineer. + Contact hidden: Contacto oculto: @@ -1443,9 +2013,9 @@ This is your own one-time link! El contacto está en línea notification - - Contact is not connected yet! - ¡El contacto aun no se ha conectado! + + Contact is deleted. + El contacto está eliminado. No comment provided by engineer. @@ -1458,6 +2028,11 @@ This is your own one-time link! 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 @@ -1468,21 +2043,41 @@ This is your own one-time link! Tus contactos sólo pueden marcar los mensajes para eliminar. Tu podrás verlos. No comment provided by engineer. + + Content violates conditions of use + El contenido viola las condiciones de uso + blocking reason + Continue Continuar No comment provided by engineer. + + Conversation deleted! + ¡Conversación eliminada! + No comment provided by engineer. + Copy Copiar - chat item action + 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 %@? @@ -1493,6 +2088,11 @@ This is your own one-time link! Crear No comment provided by engineer. + + Create 1-time link + Crear enlace de un uso + No comment provided by engineer. + Create SimpleX address Crear dirección SimpleX @@ -1503,11 +2103,6 @@ This is your own one-time link! Crear grupo usando perfil aleatorio. 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. - No comment provided by engineer. - Create file Crear archivo @@ -1528,6 +2123,11 @@ This is your own one-time 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/). 💻 @@ -1553,6 +2153,11 @@ This is your own one-time link! Crea tu perfil No comment provided by engineer. + + Created + Creadas + No comment provided by engineer. + Created at Creado @@ -1563,11 +2168,6 @@ This is your own one-time link! Creado: %@ copied message info - - Created on %@ - Creado en %@ - No comment provided by engineer. - Creating archive link Creando enlace al archivo @@ -1583,11 +2183,21 @@ This is your own one-time link! Código de Acceso 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 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 %@. @@ -1598,11 +2208,26 @@ This is your own one-time link! 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 ID ID base de datos @@ -1669,7 +2294,7 @@ This is your own one-time link! Database passphrase is different from saved in the keychain. - La contraseña es distinta a la almacenada en Keychain. + La contraseña es diferente a la almacenada en Keychain. No comment provided by engineer. @@ -1701,6 +2326,11 @@ This is your own one-time link! La base de datos migrará cuando se reinicie la aplicación No comment provided by engineer. + + Debug delivery + Informe debug + No comment provided by engineer. + Decentralized Descentralizada @@ -1714,16 +2344,17 @@ This is your own one-time link! Delete Eliminar - chat item action + alert action +swipe action + + + Delete %lld messages of members? + ¿Eliminar %lld mensajes de miembros? + No comment provided by engineer. Delete %lld messages? - ¿Elimina %lld mensajes? - No comment provided by engineer. - - - Delete Contact - Eliminar contacto + ¿Eliminar %lld mensajes? No comment provided by engineer. @@ -1751,14 +2382,14 @@ This is your own one-time link! Eliminar y notificar contacto No comment provided by engineer. - - Delete archive - Eliminar archivo + + Delete chat + Eliminar chat No comment provided by engineer. - - Delete chat archive? - ¿Eliminar archivo del chat? + + Delete chat messages from your device. + Elimina los mensajes del dispositivo. No comment provided by engineer. @@ -1771,6 +2402,11 @@ This is your own one-time link! ¿Eliminar perfil? No comment provided by engineer. + + Delete chat? + ¿Eliminar chat? + No comment provided by engineer. + Delete connection Eliminar conexión @@ -1781,11 +2417,9 @@ This is your own one-time link! Eliminar contacto No comment provided by engineer. - - Delete contact? -This cannot be undone! - ¿Eliminar contacto? -¡No podrá deshacerse! + + Delete contact? + ¿Eliminar contacto? No comment provided by engineer. @@ -1805,7 +2439,7 @@ This cannot be undone! Delete files and media? - Eliminar archivos y multimedia? + ¿Eliminar archivos y multimedia? No comment provided by engineer. @@ -1848,6 +2482,11 @@ This cannot be undone! ¿Eliminar enlace? No comment provided by engineer. + + Delete list? + ¿Eliminar lista? + alert title + Delete member message? ¿Eliminar el mensaje de miembro? @@ -1860,8 +2499,8 @@ This cannot be undone! Delete messages - Eliminar mensaje - No comment provided by engineer. + Activar + alert button Delete messages after @@ -1878,9 +2517,9 @@ This cannot be undone! ¿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. @@ -1898,11 +2537,31 @@ This cannot be undone! Eliminar cola server test step + + 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 + No comment provided by engineer. + + + Deleted + Eliminadas + No comment provided by engineer. + Deleted at Eliminado @@ -1913,6 +2572,16 @@ This cannot be undone! Eliminado: %@ copied message info + + 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 @@ -1948,11 +2617,41 @@ This cannot be undone! 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 %@. + No comment provided by engineer. + + + Destination server error: %@ + Error del servidor de destino: %@ + snd error text + + + 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 @@ -1983,8 +2682,13 @@ This cannot be undone! Mensajes directos chat feature - - Direct messages between members are prohibited in this group. + + 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. @@ -1998,11 +2702,26 @@ This cannot be undone! Desactivar Bloqueo SimpleX authentication reason + + Disable automatic message deletion? + ¿Desactivar la eliminación automática de mensajes? + alert title + + + Disable delete messages + Desactivar + alert button + Disable for all Desactivar para todos No comment provided by engineer. + + Disabled + Desactivado + No comment provided by engineer. + Disappearing message Mensaje temporal @@ -2018,8 +2737,8 @@ This cannot be undone! Los mensajes temporales no están permitidos en este chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Los mensajes temporales no están permitidos en este grupo. No comment provided by engineer. @@ -2040,7 +2759,7 @@ This cannot be undone! Disconnect desktop? - ¿Desconectar ordenador? + ¿Desconectar del ordenador? No comment provided by engineer. @@ -2053,11 +2772,21 @@ This cannot be undone! Descubrir en red local 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 SimpleX for emergency calls. NO uses SimpleX para llamadas de emergencia. No comment provided by engineer. + + Do NOT use private routing. + NO usar enrutamiento privado. + No comment provided by engineer. + Do it later Hacer más tarde @@ -2065,7 +2794,17 @@ This cannot be undone! Do not send history to new members. - No enviar historial a miembros nuevos. + No se envía el historial a los miembros nuevos. + No comment provided by engineer. + + + Do not use credentials with proxy. + No uses credenciales con proxy. + No comment provided by engineer. + + + Documents: + Documentos: No comment provided by engineer. @@ -2078,9 +2817,19 @@ This cannot be undone! No activar No comment provided by engineer. + + Don't miss important messages. + No pierdas los mensajes importantes. + No comment provided by engineer. + Don't show again - No mostrar de nuevo + No volver a mostrar + No comment provided by engineer. + + + Done + Hecho No comment provided by engineer. @@ -2091,7 +2840,13 @@ This cannot be undone! Download Descargar - chat item action + alert button +chat item action + + + Download errors + Errores en la descarga + No comment provided by engineer. Download failed @@ -2103,6 +2858,21 @@ This cannot be undone! Descargar archivo server test step + + Download files + Descargar archivos + alert action + + + Downloaded + Descargado + No comment provided by engineer. + + + Downloaded files + Archivos descargados + No comment provided by engineer. + Downloading archive Descargando archivo @@ -2123,6 +2893,11 @@ This cannot be undone! Duración No comment provided by engineer. + + E2E encrypted notifications. + Notificaciones cifradas E2E. + No comment provided by engineer. + Edit Editar @@ -2143,6 +2918,11 @@ This cannot be undone! Activar (conservar anulaciones) 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 SimpleX Lock Activar Bloqueo SimpleX @@ -2156,7 +2936,7 @@ This cannot be undone! Enable automatic message deletion? ¿Activar eliminación automática de mensajes? - No comment provided by engineer. + alert title Enable camera access @@ -2203,9 +2983,14 @@ This cannot be undone! Activar código de autodestrucción set passcode view + + Enabled + Activado + No comment provided by engineer. + Enabled for - Activar para + Activado para No comment provided by engineer. @@ -2220,7 +3005,7 @@ This cannot be undone! Encrypt local files - Cifra archivos locales + Cifrar archivos locales No comment provided by engineer. @@ -2278,6 +3063,11 @@ This cannot be undone! Renegociación de cifrado fallida. No comment provided by engineer. + + Encryption renegotiation in progress. + Renegociación de cifrado en curso. + No comment provided by engineer. + Enter Passcode Introduce Código @@ -2310,7 +3100,7 @@ This cannot be undone! Enter server manually - Introduce el servidor manualmente + Añadir manualmente No comment provided by engineer. @@ -2343,26 +3133,36 @@ This cannot be undone! Error al cancelar cambio de dirección No comment provided by engineer. + + Error accepting conditions + Error al aceptar las condiciones + alert title + 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) No comment provided by engineer. + + Error adding server + Error al añadir servidor + alert title + 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 @@ -2373,6 +3173,21 @@ This cannot be undone! 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 @@ -2388,6 +3203,11 @@ This cannot be undone! Error al crear enlace de grupo No comment provided by engineer. + + Error creating list + Error al crear lista + alert title + Error creating member contact Error al establecer contacto con el miembro @@ -2403,6 +3223,11 @@ This cannot be undone! ¡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 @@ -2423,11 +3248,6 @@ This cannot be undone! 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 @@ -2473,6 +3293,11 @@ This cannot be undone! 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 @@ -2483,9 +3308,14 @@ This cannot be undone! Error al unirte al grupo No comment provided by engineer. - - Error loading %@ servers - Error al cargar servidores %@ + + Error loading servers + Error al cargar servidores + alert title + + + Error migrating settings + Error al migrar la configuración No comment provided by engineer. @@ -2496,16 +3326,36 @@ This cannot be undone! Error receiving file Error al recibir archivo + alert title + + + Error reconnecting server + Error al reconectar con el servidor No comment provided by engineer. + + Error reconnecting servers + Error al reconectar con los servidores + No comment provided by engineer. + + + Error registering for notifications + Error al registrarse para notificaciones + alert title + Error removing member - Error al eliminar miembro + Error al expulsar miembro No comment provided by engineer. - - Error saving %@ servers - Error al guardar servidores %@ + + Error reordering lists + Error al reorganizar listas + alert title + + + Error resetting statistics + Error al restablecer las estadísticas No comment provided by engineer. @@ -2513,6 +3363,11 @@ This cannot be undone! Error al guardar servidores ICE No comment provided by engineer. + + Error saving chat list + Error al guardar listas + alert title + Error saving group profile Error al guardar perfil de grupo @@ -2528,6 +3383,11 @@ This cannot be undone! Error al guardar contraseña en Keychain No comment provided by engineer. + + Error saving servers + Error al guardar servidores + alert title + Error saving settings Error al guardar ajustes @@ -2570,19 +3430,29 @@ This cannot be undone! Error stopping chat - Error al parar Chat + Error al parar SimpleX + No comment provided by engineer. + + + Error switching profile + Error al cambiar perfil No comment provided by engineer. Error switching profile! ¡Error al cambiar perfil! - No comment provided by engineer. + alertTitle 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 @@ -2593,6 +3463,11 @@ This cannot be undone! Error al actualizar mensaje No comment provided by engineer. + + Error updating server + Error al actualizar el servidor + alert title + Error updating settings Error al actualizar configuración @@ -2621,7 +3496,9 @@ This cannot be undone! Error: %@ Error: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3510,16 @@ This cannot be undone! Error: sin archivo de base de datos No comment provided by engineer. + + Errors + Errores + No comment provided by engineer. + + + Errors in servers configuration. + Error en la configuración del servidor. + servers error + Even when disabled in the conversation. Incluso si está desactivado para la conversación. @@ -2648,6 +3535,11 @@ This cannot be undone! Expandir chat item action + + Expired + Expirado + token status text + Export database Exportar base de datos @@ -2658,6 +3550,11 @@ This cannot be undone! 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. @@ -2683,16 +3580,70 @@ This cannot be undone! ¡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. + No comment provided by engineer. + Favorite Favoritos + swipe action + + + Favorites + Favoritos No comment provided by engineer. + + File error + Error de archivo + file error alert title + + + File errors: +%@ + Error(es) de archivo +%@ + alert message + + + File is blocked by server operator: +%@. + Archivo bloqueado por el operador del servidor +%@. + 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: %@ + file error text + + + File status + Estado del archivo + No comment provided by engineer. + + + File status: %@ + Estado del archivo: %@ + copied message info + File will be deleted from servers. El archivo será eliminado de los servidores. @@ -2705,7 +3656,7 @@ This cannot be undone! 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 compruébalo más tarde! + El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde! No comment provided by engineer. @@ -2713,6 +3664,11 @@ This cannot be undone! Archivo: %@ No comment provided by engineer. + + Files + Archivos + No comment provided by engineer. + Files & media Archivos y multimedia @@ -2723,8 +3679,8 @@ This cannot be undone! Archivos y multimedia chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Los archivos y multimedia no están permitidos en este grupo. No comment provided by engineer. @@ -2793,21 +3749,71 @@ This cannot be undone! Corrección no compatible con miembro del grupo No comment provided by engineer. + + For all moderators + Para todos los moderadores + No comment provided by engineer. + + + For chat profile %@: + Para el perfil de chat %@: + servers error + 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 + No comment provided by engineer. + Forward Reenviar chat item action + + Forward %d message(s)? + ¿Reenviar %d mensaje(s)? + alert title + Forward and save messages Reenviar y guardar mensajes No comment provided by engineer. + + Forward messages + Reenviar mensajes + alert action + + + Forward messages without files? + ¿Reenviar mensajes sin los archivos? + alert message + + + Forward up to 20 messages at once. + Desplazamiento de hasta 20 mensajes. + No comment provided by engineer. + Forwarded Reenviado @@ -2818,6 +3824,40 @@ This cannot be undone! 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Servidor de reenvío: %1$@ +Error del servidor de destino: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Servidor de reenvío: %1$@ +Error: %2$@ + snd error text + Found desktop Ordenador encontrado @@ -2838,19 +3878,14 @@ This cannot be undone! Nombre completo (opcional) No comment provided by engineer. - - Full name: - Nombre completo: - No comment provided by engineer. - Fully decentralized – visible only to members. - Completamente descentralizado: sólo visible a los miembros. + 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! + Totalmente revisado. ¡Funciona en segundo plano! No comment provided by engineer. @@ -2863,6 +3898,21 @@ This cannot be undone! GIFs y stickers No comment provided by engineer. + + Get notified when mentioned. + Las menciones ahora se notifican. + No comment provided by engineer. + + + Good afternoon! + ¡Buenas tardes! + message preview + + + Good morning! + ¡Buenos días! + message preview + Group Grupo @@ -2918,41 +3968,6 @@ This cannot be undone! 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. (24 hours) - Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas) - No comment provided by engineer. - - - Group members can send SimpleX links. - Los miembros del grupo pueden enviar enlaces SimpleX. - 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. - No comment provided by engineer. - Group message: Mensaje de grupo: @@ -2985,12 +4000,17 @@ This cannot be undone! Group will be deleted for all members - this cannot be undone! - El grupo será eliminado para todos los miembros. ¡No podrá deshacerse! + 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! + El grupo será eliminado para tí. ¡No puede deshacerse! + No comment provided by engineer. + + + Groups + Grupos No comment provided by engineer. @@ -2998,6 +4018,11 @@ This cannot be undone! 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 @@ -3048,10 +4073,20 @@ This cannot be undone! Cómo funciona SimpleX 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 + No comment provided by engineer. + How it works Cómo funciona - No comment provided by engineer. + alert button How to @@ -3078,9 +4113,14 @@ This cannot be undone! Servidores ICE (uno por línea) No comment provided by engineer. + + IP address + Dirección IP + 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. + Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace. No comment provided by engineer. @@ -3110,7 +4150,7 @@ This cannot be undone! 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 compruébalo más tarde! + La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde! No comment provided by engineer. @@ -3118,8 +4158,8 @@ This cannot be undone! Inmediatamente No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Inmune a spam y abuso No comment provided by engineer. @@ -3143,11 +4183,23 @@ This cannot be undone! 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. +More improvements are coming soon! + Reducción del tráfico y entrega mejorada. +¡Pronto habrá nuevas mejoras! + No comment provided by engineer. + Improved message delivery Entrega de mensajes mejorada @@ -3165,7 +4217,7 @@ This cannot be undone! In order to continue, chat should be stopped. - Para continuar, Chat debe estar parado. + Para continuar, SimpleX debe estar parado. No comment provided by engineer. @@ -3178,6 +4230,16 @@ This cannot be undone! Sonido de llamada No comment provided by engineer. + + Inappropriate content + Contenido inapropiado + report reason + + + Inappropriate profile + Perfil inapropiado + report reason + Incognito Incógnito @@ -3248,6 +4310,11 @@ This cannot be undone! 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! @@ -3255,16 +4322,41 @@ This cannot be undone! No comment provided by engineer. - - Instantly - Al instante - No comment provided by engineer. - Interface Interfaz No comment provided by engineer. + + Interface colors + Colores del interfaz + No comment provided by engineer. + + + 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) + token status text + Invalid QR code Código QR no válido @@ -3303,7 +4395,7 @@ This cannot be undone! Invalid server address! ¡Dirección de servidor no válida! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4417,11 @@ This cannot be undone! Invitar miembros No comment provided by engineer. + + Invite to chat + Invitar al chat + No comment provided by engineer. + Invite to group Invitar al grupo @@ -3340,8 +4437,8 @@ This cannot be undone! 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. + + Irreversible message deletion is prohibited. La eliminación irreversible de mensajes no está permitida en este grupo. No comment provided by engineer. @@ -3366,6 +4463,11 @@ This cannot be undone! 3. 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 (%@). @@ -3384,7 +4486,7 @@ This cannot be undone! Join Unirte - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4528,11 @@ This is your link for group %@! Keep Guardar + alert action + + + Keep conversation + Conservar conversación No comment provided by engineer. @@ -3436,7 +4543,7 @@ This is your link for group %@! Keep unused invitation? ¿Guardar invitación no usada? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4578,16 @@ This is your link for group %@! Leave Salir + swipe action + + + Leave chat + Salir del chat + No comment provided by engineer. + + + Leave chat? + ¿Salir del chat? No comment provided by engineer. @@ -3513,6 +4630,21 @@ This is your link for group %@! Ordenadores enlazados No comment provided by engineer. + + List + Lista + swipe action + + + 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 message! ¡Mensaje en vivo! @@ -3523,11 +4655,6 @@ This is your link for group %@! Mensajes en vivo No comment provided by engineer. - - Local - Local - No comment provided by engineer. - Local name Nombre local @@ -3535,7 +4662,7 @@ This is your link for group %@! Local profile data only - Sólo datos del perfil local + Eliminar sólo el perfil No comment provided by engineer. @@ -3548,11 +4675,6 @@ This is your link for group %@! 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 @@ -3563,21 +4685,11 @@ This is your link for group %@! ¡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 @@ -3603,11 +4715,36 @@ This is your link for group %@! Máximo 30 segundos, recibido al instante. No comment provided by engineer. + + Media & file servers + Servidores de archivos y multimedia + No comment provided by engineer. + + + Medium + Medio + blur media + Member Miembro No comment provided by engineer. + + Member inactive + Miembro inactivo + item status text + + + Member reports + Informes de miembros + chat feature + + + 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 role will be changed to "%@". All group members will be notified. El rol del miembro cambiará a "%@" y se notificará al grupo. @@ -3618,9 +4755,64 @@ This is your link for group %@! 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 podrá deshacerse! + 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 SimpleX links. + Los miembros del grupo pueden enviar enlaces SimpleX. + 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 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. @@ -3633,11 +4825,31 @@ This is your link for group %@! ¡Confirmación de entrega de mensajes! No comment provided by engineer. + + Message delivery warning + Aviso de entrega de mensaje + item status text + Message draft Borrador de mensaje No comment provided by engineer. + + Message forwarded + Mensaje reenviado + item status text + + + 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. + item status description + + + Message queue info + Información cola de mensajes + No comment provided by engineer. + Message reactions Reacciones a mensajes @@ -3648,16 +4860,41 @@ This is your link for group %@! Las reacciones a los mensajes no están permitidas en este chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Las reacciones a los mensajes no están permitidas en este grupo. 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 + No comment provided by engineer. + + + Message status: %@ + Estado del mensaje: %@ + copied message info + Message text Contacto y texto @@ -3683,6 +4920,26 @@ This is your link for group %@! ¡Los mensajes de %@ serán mostrados! No comment provided by engineer. + + Messages in this chat will never be deleted. + Los mensajes de esta conversación nunca se eliminan. + alert message + + + Messages received + Mensajes recibidos + No comment provided by engineer. + + + Messages sent + Mensajes enviados + No comment provided by engineer. + + + Messages were deleted after you selected them. + Los mensajes han sido eliminados después de seleccionarlos. + alert message + 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. @@ -3748,9 +5005,9 @@ This is your link for group %@! Migración completada No comment provided by engineer. - - Migrations: %@ - Migraciones: %@ + + Migrations: + Migraciones: No comment provided by engineer. @@ -3768,6 +5025,11 @@ This is your link for group %@! Moderado: %@ copied message info + + More + Más + swipe action + More improvements are coming soon! ¡Pronto habrá más mejoras! @@ -3778,16 +5040,16 @@ This is your link for group %@! Conexión de red más fiable. No comment provided by engineer. + + More reliable notifications + Notificaciones más fiables + No comment provided by engineer. + Most likely this connection is deleted. Probablemente la conexión ha sido eliminada. item status description - - 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 @@ -3796,7 +5058,12 @@ This is your link for group %@! Mute Silenciar - No comment provided by engineer. + notification label action + + + Mute all + Silenciar todo + notification label action Muted when inactive! @@ -3806,7 +5073,7 @@ This is your link for group %@! Name Nombre - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +5085,26 @@ This is your link for group %@! Conexión de red No comment provided by engineer. + + Network decentralization + Descentralización de la red + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Problema en la red - el mensaje ha expirado tras muchos intentos de envío. + snd error text + 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 @@ -3833,16 +5115,36 @@ This is your link for group %@! Estado de la red No comment provided by engineer. + + New + Nuevo + token status text + New Passcode Código 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. + No comment provided by engineer. + New chat Nuevo chat No comment provided by engineer. + + New chat experience 🎉 + Nueva experiencia de chat 🎉 + No comment provided by engineer. + New contact request Nueva solicitud de contacto @@ -3853,11 +5155,6 @@ This is your link for group %@! Contacto nuevo: notification - - New database archive - Nuevo archivo de bases de datos - No comment provided by engineer. - New desktop app! Nueva aplicación para PC! @@ -3868,11 +5165,21 @@ This is your link for group %@! Nuevo nombre mostrado No comment provided by engineer. + + New events + Eventos nuevos + notification + 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 @@ -3888,6 +5195,11 @@ This is your link for group %@! Contraseña nueva… No comment provided by engineer. + + New server + Servidor nuevo + No comment provided by engineer. + No No @@ -3898,6 +5210,21 @@ This is your link for group %@! Sin contraseña de la aplicación Authentication unavailable + + 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 @@ -3918,6 +5245,11 @@ This is your link for group %@! ¡Sin dispositivo token! No comment provided by engineer. + + 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. + item status description + No filtered chats Sin chats filtrados @@ -3933,26 +5265,111 @@ This is your link for group %@! Sin historial No comment provided by engineer. + + No info, try to reload + No hay información, intenta recargar + No comment provided by engineer. + + + No media & file servers. + Sin servidores para archivos y multimedia. + servers error + + + No message + Ningún mensaje + No comment provided by engineer. + + + No message servers. + Sin servidores para mensajes. + servers error + 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 No comment provided by engineer. + + 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. + servers error + + + No token! + ¡Sin token! + alert title + + + 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 + No comment provided by engineer. + + + Nothing to forward! + ¡Nada para reenviar! + alert title + Notifications Notificaciones @@ -3963,6 +5380,21 @@ This is your link for group %@! ¡Las notificaciones están desactivadas! No comment provided by engineer. + + Notifications error + Error en notificaciones + alert title + + + Notifications privacy + Privacidad en las notificaciones + No comment provided by engineer. + + + Notifications status + Estado notificaciones + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5412,35 @@ This is your link for group %@! Off Desactivado - No comment provided by engineer. + blur media Ok Ok - No comment provided by engineer. + alert button Old database Base de datos antigua No comment provided by engineer. - - Old database archive - Archivo de bases de datos antiguas - No comment provided by engineer. - One-time invitation link - Enlace único de invitación de un uso + 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. +Requires compatible VPN. + Se **requieren** hosts .onion para la conexión. +Requiere 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. +Requires compatible VPN. + Se usarán hosts .onion si están disponibles. +Requiere activación de la VPN. No comment provided by engineer. @@ -4017,11 +5448,21 @@ This is your link for group %@! 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**. + + Only chat owners can change preferences. + Sólo los propietarios del chat pueden cambiar las preferencias. + No comment provided by engineer. + + + 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. @@ -4037,6 +5478,16 @@ This is your link for group %@! 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. @@ -4090,13 +5541,18 @@ This is your link for group %@! Open Abrir - No comment provided by engineer. + alert action Open Settings Abrir Configuración No comment provided by engineer. + + Open changes + Abrir cambios + No comment provided by engineer. + Open chat Abrir chat @@ -4107,31 +5563,45 @@ This is your link for group %@! Abrir consola de Chat authentication reason + + Open conditions + Abrir condiciones + No comment provided by engineer. + Open group Grupo abierto No comment provided by engineer. + + Open link? + alert title + Open migration to another device Abrir menú migración a otro dispositivo authentication reason - - Open user profiles - Abrir perfil de usuario - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Protocolo y código abiertos: cualquiera puede usar los servidores. - No comment provided by engineer. - Opening app… Iniciando aplicación… No comment provided by engineer. + + Operator + Operador + No comment provided by engineer. + + + Operator server + Servidor del operador + alert title + + + Or import archive file + O importa desde un archivo + No comment provided by engineer. + Or paste archive link O pegar enlace del archivo @@ -4139,7 +5609,7 @@ This is your link for group %@! Or scan QR code - O escanear código QR + O escanea el código QR No comment provided by engineer. @@ -4149,7 +5619,17 @@ This is your link for group %@! Or show this code - O mostrar este código + 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. @@ -4157,6 +5637,13 @@ This is your link for group %@! Otro No comment provided by engineer. + + Other file errors: +%@ + Otro(s) error(es) de archivo. +%@ + alert message + PING count Contador PING @@ -4192,6 +5679,11 @@ This is your link for group %@! ¡Código de acceso guardado! No comment provided by engineer. + + Password + Contraseña + No comment provided by engineer. + Password to show Contraseña para hacerlo visible @@ -4219,17 +5711,17 @@ This is your link for group %@! Paste the link you received - Pegar el enlace recibido + Pega el enlace recibido 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 + Pendientes No comment provided by engineer. - - Periodically - Periódico + + Periodic + Periódicamente No comment provided by engineer. @@ -4242,11 +5734,28 @@ This is your link for group %@! Llamadas picture-in-picture 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. +Please 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. +Por 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. @@ -4314,14 +5823,34 @@ Error: %@ Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes. No comment provided by engineer. + + 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. + token info + Polish interface Interfaz en polaco No comment provided by engineer. + + Port + Puerto + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect - Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta + Posiblemente la huella del certificado en la dirección del servidor es incorrecta server test error @@ -4329,14 +5858,14 @@ Error: %@ Conserva el último borrador del mensaje con los datos adjuntos. No comment provided by engineer. - - Preset server - Servidor predefinido - No comment provided by engineer. - Preset server address - Dirección del servidor predefinida + Dirección predefinida del servidor + No comment provided by engineer. + + + Preset servers + Servidores predefinidos No comment provided by engineer. @@ -4344,9 +5873,24 @@ Error: %@ Vista previa No comment provided by engineer. + + Previously connected servers + Servidores conectados previamente + No comment provided by engineer. + Privacy & security - Privacidad y Seguridad + 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. @@ -4354,19 +5898,49 @@ Error: %@ 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. + + 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 🚀 + No comment provided by engineer. + Private notes Notas privadas name of notes to self + + 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 - Datos del perfil y conexiones + Eliminar perfil y conexiones No comment provided by engineer. @@ -4376,17 +5950,7 @@ Error: %@ Profile images - Imágenes del perfil - No comment provided by engineer. - - - Profile name - Nombre del perfil - No comment provided by engineer. - - - Profile name: - Nombre del perfil: + Forma de los perfiles No comment provided by engineer. @@ -4394,10 +5958,15 @@ Error: %@ Contraseña del perfil No comment provided by engineer. + + Profile theme + Tema del perfil + No comment provided by engineer. + Profile update will be sent to your contacts. La actualización del perfil se enviará a tus contactos. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,9 +5988,14 @@ Error: %@ 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 SimpleX links. - No permitir el envío de enlaces SimpleX. + No se permite enviar enlaces SimpleX. No comment provided by engineer. @@ -4444,9 +6018,21 @@ Error: %@ No se permiten mensajes de voz. No comment provided by engineer. + + Protect IP address + Proteger dirección IP + No comment provided by engineer. + Protect app screen - Proteger la pantalla de la aplicación + Proteger la pantalla + No comment provided by engineer. + + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Protege tu dirección IP de los servidores de retransmisión elegidos por tus contactos. +Actívalo en ajustes de *Servidores y Redes*. No comment provided by engineer. @@ -4456,17 +6042,32 @@ Error: %@ Protocol timeout - Tiempo de espera del protocolo + Timeout protocolo No comment provided by engineer. Protocol timeout per KB - Límite de espera del protocolo por 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 automáticas + Notificaciones push No comment provided by engineer. @@ -4484,6 +6085,11 @@ Error: %@ Valora la aplicación No comment provided by engineer. + + Reachable chat toolbar + Barra de menú accesible + No comment provided by engineer. + React… Reacciona… @@ -4492,36 +6098,31 @@ Error: %@ Read Leer - No comment provided by engineer. + swipe action 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). - Saber más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Saber más en [Guía de Usuario](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/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). No comment provided by engineer. Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Saber 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. - Saber más en nuestro repositorio GitHub. + Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - Saber más en nuestro [repositorio GitHub](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. @@ -4529,6 +6130,11 @@ Error: %@ Las confirmaciones están desactivadas No comment provided by engineer. + + Receive errors + Errores de recepción + No comment provided by engineer. + Received at Recibido a las @@ -4549,16 +6155,26 @@ Error: %@ Mensaje entrante message info title + + 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. No comment provided by engineer. - - Receiving concurrency - Concurrencia en la recepción - No comment provided by engineer. - Receiving file will be stopped. Se detendrá la recepción del archivo. @@ -4584,11 +6200,36 @@ Error: %@ 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? @@ -4609,10 +6250,26 @@ Error: %@ Reducción del uso de batería No comment provided by engineer. + + Register + Registrar + No comment provided by engineer. + + + Register notification token? + ¿Registrar el token de notificaciones? + token info + + + Registered + Registrado + token status text + Reject Rechazar - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4626,7 +6283,7 @@ Error: %@ 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. + 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. @@ -4639,6 +6296,16 @@ Error: %@ 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 @@ -4699,6 +6366,56 @@ Error: %@ Responder chat item action + + Report + Informe + chat item action + + + 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. + report reason + + + Report reason? + ¿Motivo del informe? + No comment provided by engineer. + + + 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 reason + + + Report: %@ + Informe: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No se permite informar de mensajes a los moderadores. + No comment provided by engineer. + + + Reports + Informes + No comment provided by engineer. + Required Obligatorio @@ -4709,14 +6426,39 @@ Error: %@ 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 app theme + Restablecer al tema de la aplicación + No comment provided by engineer. + Reset to defaults - Restablecer valores por defecto + Restablecer valores predetarminados + No comment provided by engineer. + + + Reset to user theme + Restablecer al tema del usuario No comment provided by engineer. @@ -4759,9 +6501,9 @@ Error: %@ Revelar chat item action - - Revert - Revertir + + Review conditions + Revisar condiciones No comment provided by engineer. @@ -4786,12 +6528,22 @@ Error: %@ Run chat - Ejecutar chat + Ejecutar SimpleX No comment provided by engineer. - - SMP servers - Servidores SMP + + SMP server + Servidor SMP + No comment provided by engineer. + + + SOCKS proxy + Proxy SOCKS + No comment provided by engineer. + + + Safely receive files + Recibe archivos de forma segura No comment provided by engineer. @@ -4802,43 +6554,44 @@ Error: %@ Save Guardar - chat item action + alert button +chat item action 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. + alert button Save and notify group members Guardar y notificar grupo No comment provided by engineer. + + Save and reconnect + Guardar y reconectar + No comment provided by engineer. + Save and update group profile Guardar y actualizar perfil del grupo No comment provided by engineer. - - Save archive - Guardar archivo - No comment provided by engineer. - - - Save auto-accept settings - Guardar configuración de auto aceptar - 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 @@ -4852,7 +6605,7 @@ Error: %@ Save preferences? ¿Guardar preferencias? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6620,18 @@ Error: %@ Save servers? ¿Guardar servidores? - No comment provided by engineer. - - - Save settings? - ¿Guardar configuración? - No comment provided by engineer. + alert title Save welcome message? ¿Guardar mensaje de bienvenida? No comment provided by engineer. + + Save your profile? + ¿Guardar tu perfil? + alert title + Saved Guardado @@ -4899,6 +6652,21 @@ Error: %@ Mensaje guardado message info title + + 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 QR code Escanear código QR @@ -4921,7 +6689,7 @@ Error: %@ Scan server QR code - Escanear código QR del servidor + Escanear código QR No comment provided by engineer. @@ -4939,11 +6707,21 @@ Error: %@ Buscar o pegar enlace SimpleX No comment provided by engineer. + + Secondary + Secundario + No comment provided by engineer. + Secure queue Cola segura server test step + + Secured + Aseguradas + No comment provided by engineer. + Security assessment Evaluación de la seguridad @@ -4957,6 +6735,21 @@ Error: %@ Select Seleccionar + chat item action + + + 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. @@ -4994,14 +6787,9 @@ Error: %@ Enviar confirmaciones de entrega a 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 + Envía un mensaje para conectar No comment provided by engineer. @@ -5009,6 +6797,11 @@ Error: %@ 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 @@ -5019,14 +6812,29 @@ Error: %@ 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. @@ -5046,13 +6854,13 @@ Error: %@ Send up to 100 last messages to new members. - Enviar hasta 100 últimos mensajes a los miembros nuevos. + Se envían hasta 100 mensajes más recientes a los miembros nuevos. No comment provided by engineer. Sender cancelled file transfer. El remitente ha cancelado la transferencia de archivos. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6917,11 @@ Error: %@ Enviado: %@ copied message info + + Sent directly + Directamente + No comment provided by engineer. + Sent file event Evento de archivo enviado @@ -5119,11 +6932,71 @@ Error: %@ Mensaje saliente message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + Servidor añadido al operador %@. + alert message + + + 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: %@. + No comment provided by engineer. + + + Server operator changed. + El operador del servidor ha cambiado. + alert title + + + Server operators + Operadores de servidores + No comment provided by engineer. + + + Server protocol changed. + El protocolo del servidor ha cambiado. + alert title + Server requires authorization to create queues, check password El servidor requiere autorización para crear colas, comprueba la contraseña @@ -5136,7 +7009,22 @@ Error: %@ Server test failed! - ¡Error en prueba del servidor! + ¡Prueba no superada! + No comment provided by engineer. + + + Server type + Tipo de servidor + No comment provided by engineer. + + + Server version is incompatible with network settings. + La versión del servidor es incompatible con la configuración de red. + srv error text + + + Server version is incompatible with your app: %@. + La versión del servidor es incompatible con tu aplicación: %@. No comment provided by engineer. @@ -5144,6 +7032,16 @@ Error: %@ 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 @@ -5154,11 +7052,21 @@ Error: %@ 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 @@ -5169,6 +7077,11 @@ Error: %@ Ú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. + No comment provided by engineer. + Set passcode Código autodestrucción @@ -5199,6 +7112,11 @@ Error: %@ Configuración No comment provided by engineer. + + Settings were changed. + La configuración ha sido modificada. + alert message + Shape profile images Dar forma a las imágenes de perfil @@ -5207,21 +7125,42 @@ Error: %@ Share Compartir - chat item action + alert action +chat item action 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 SimpleX address on social media. + Comparte tu dirección SimpleX en redes sociales. + No comment provided by engineer. + Share address Compartir dirección No comment provided by engineer. + + Share address publicly + Campartir dirección públicamente + No comment provided by engineer. + Share address with contacts? ¿Compartir la dirección con los contactos? + alert title + + + Share from other apps. + Comparte desde otras aplicaciones. No comment provided by engineer. @@ -5229,9 +7168,19 @@ Error: %@ Compartir enlace No comment provided by engineer. + + Share profile + Perfil a compartir + No comment provided by engineer. + Share this 1-time invite link - Compartir este enlace de un uso + Comparte este enlace de un solo uso + No comment provided by engineer. + + + Share to SimpleX + Compartir con Simplex No comment provided by engineer. @@ -5239,6 +7188,11 @@ Error: %@ Compartir con contactos No comment provided by engineer. + + Short link + Enlace corto + No comment provided by engineer. + Show QR code Mostrar código QR @@ -5259,21 +7213,46 @@ Error: %@ 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 → on messages sent via private routing. + Mostrar → en mensajes con enrutamiento privado. + 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 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. @@ -5304,6 +7283,21 @@ Error: %@ 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? + No comment provided by engineer. + + + SimpleX channel link + Enlace de canal SimpleX + simplex link type + SimpleX contact address Dirección de contacto SimpleX @@ -5324,8 +7318,8 @@ Error: %@ Enlaces SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. Los enlaces SimpleX no se permiten en este grupo. No comment provided by engineer. @@ -5339,11 +7333,21 @@ Error: %@ Invitación SimpleX de un uso simplex link type + + 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 @@ -5356,7 +7360,22 @@ Error: %@ Small groups (max 20) - Grupos pequeños (máx. 20) + Grupos pequeños (max. 20) + No comment provided by engineer. + + + Soft + Suave + blur media + + + 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. @@ -5364,11 +7383,29 @@ Error: %@ 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + Algunos servidores no han superado la prueba: +%@ + alert message + Somebody Alguien notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Cuadrada, circular o cualquier forma intermedia. @@ -5389,6 +7426,16 @@ Error: %@ Iniciar migración No comment provided by engineer. + + Starting from %@. + Iniciado el %@. + No comment provided by engineer. + + + Statistics + Estadísticas + No comment provided by engineer. + Stop Parar @@ -5401,17 +7448,12 @@ Error: %@ Stop chat - Parar chat - No comment provided by engineer. - - - Stop chat to enable database actions - Para habilitar las acciones sobre la base de datos, debes parar Chat + Parar SimpleX 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 parar Chat. Mientras tanto no podrás recibir ni enviar mensajes. + 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. @@ -5437,28 +7479,63 @@ Error: %@ Stop sharing Dejar de compartir - No comment provided by engineer. + alert action Stop sharing address? ¿Dejar de compartir la dirección? - No comment provided by engineer. + alert title Stopping chat Parando chat No comment provided by engineer. + + Storage + Almacenamiento + No comment provided by engineer. + + + Strong + Fuerte + blur media + 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 @@ -5469,9 +7546,19 @@ Error: %@ Autenticación del sistema No comment provided by engineer. + + TCP connection + Conexión TCP + No comment provided by engineer. + TCP connection timeout - Tiempo de espera de la conexión TCP agotado + Timeout de la conexión TCP + No comment provided by engineer. + + + TCP port for messaging + Puerto TCP para mensajes No comment provided by engineer. @@ -5489,11 +7576,21 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Cola + No comment provided by engineer. + Take picture Tomar foto 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 button Pulsa el botón @@ -5521,7 +7618,7 @@ Error: %@ Tap to paste link - Pulsa para pegar enlace + Pulsa para pegar el enlacePulsa para pegar enlace No comment provided by engineer. @@ -5529,16 +7626,21 @@ Error: %@ Pulsa para escanear No comment provided by engineer. - - Tap to start a new chat - Pulsa para iniciar chat nuevo - No comment provided by engineer. + + Temporary file error + Error en archivo temporal + file error alert title Test failed at step %@. - La prueba ha fallado en el paso %@. + Prueba no superada en el paso %@. server test failure + + Test notifications + Probar notificaciones + No comment provided by engineer. + Test server Probar servidor @@ -5551,8 +7653,8 @@ Error: %@ Tests failed! - ¡Pruebas fallidas! - No comment provided by engineer. + ¡Pruebas no superadas! + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7671,6 @@ Error: %@ ¡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. - No comment provided by engineer. - 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. @@ -5586,6 +7683,16 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. 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 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. @@ -5593,7 +7700,12 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The code you scanned is not a SimpleX link QR code. - El código QR escaneado no es un enlace SimpleX. + 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. @@ -5616,6 +7728,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El cifrado funciona y un cifrado nuevo no es necesario. ¡Podría dar lugar a errores de conexión! No comment provided by engineer. + + The future of messaging + La nueva generación de mensajería privada + No comment provided by engineer. + The hash of the previous message is different. El hash del mensaje anterior es diferente. @@ -5631,9 +7748,14 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. 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. @@ -5641,9 +7763,14 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. La base de datos antigua no se eliminó durante la migración, puede eliminarse. No comment provided by engineer. - - The profile is only shared with your contacts. - El perfil sólo se comparte con tus contactos. + + 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. @@ -5658,17 +7785,32 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The servers for new connections of your current chat profile **%@**. - Lista de servidores para las conexiones nuevas de tu perfil actual **%@**. + Servidores para conexiones nuevas en tu perfil **%@**. + No comment provided by engineer. + + + The servers for new files of your current chat profile **%@**. + Servidores para enviar archivos en tu perfil **%@**. No comment provided by engineer. The text you pasted is not a SimpleX link. - El texto pegado no es un enlace SimpleX. + El texto pegado no es un enlace de SimpleX. No comment provided by engineer. - - Theme - Tema + + 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. @@ -5678,7 +7820,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. They can be overridden in contact and group settings. - Se pueden anular en la configuración de contactos. + Se puede modificar desde la configuración particular de cada grupo y contacto. No comment provided by engineer. @@ -5691,6 +7833,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Esta acción es irreversible. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Podría tardar varios minutos. No comment provided by engineer. + + 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! + alert message + 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. @@ -5736,11 +7883,31 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. ¡Este es tu propio enlace de un solo uso! 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: @@ -5761,9 +7928,9 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. 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 tu 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. @@ -5771,6 +7938,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,6 +7950,26 @@ You will be prompted to complete authentication before this feature is enabled.< Se te pedirá que completes la autenticación antes de activar esta función. 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. @@ -5788,26 +7980,61 @@ Se te pedirá que completes la autenticación antes de activar esta función.
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 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. No comment provided by engineer. + + Token status: %@. + Estado token: %@. + token status + + + 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: %@). @@ -5855,7 +8082,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.
Unblock member for all? - ¿Desbloquear miembro para todos? + ¿Desbloquear el miembro para todos? No comment provided by engineer. @@ -5863,10 +8090,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.
¿Desbloquear miembro? No comment provided by engineer. - - Unexpected error: %@ - Error inesperado: %@ - item status description + + Undelivered messages + Mensajes no entregados + No comment provided by engineer. Unexpected migration state @@ -5876,7 +8103,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.
Unfav. No fav. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +8140,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.Error desconocido No comment provided by engineer. + + Unknown servers! + ¡Servidores desconocidos! + alert title + 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. @@ -5921,9 +8153,8 @@ Se te pedirá que completes la autenticación antes de activar esta función. 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. - 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. + 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. No comment provided by engineer. @@ -5949,11 +8180,16 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Unmute Activar audio - No comment provided by engineer. + notification label action Unread No leído + swipe action + + + Unsupported connection link + Enlace de conexión no compatible No comment provided by engineer. @@ -5966,11 +8202,6 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Actualizar No comment provided by engineer. - - Update .onion hosts setting? - ¿Actualizar la configuración de los hosts .onion? - No comment provided by engineer. - Update database passphrase Actualizar contraseña de la base de datos @@ -5981,19 +8212,19 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb ¿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 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. - 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. + Para actualizar la configuración el cliente se reconectará a todos los servidores. No comment provided by engineer. @@ -6001,6 +8232,11 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Actualizar y abrir Chat No comment provided by engineer. + + Upload errors + Errores en subida + No comment provided by engineer. + Upload failed Error de subida @@ -6011,21 +8247,51 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Subir archivo server test step + + 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 %@ + Usar %@ + No comment provided by engineer. + Use .onion hosts Usar hosts .onion No comment provided by engineer. + + Use SOCKS proxy + Usar proxy SOCKS + No comment provided by engineer. + Use SimpleX Chat servers? ¿Usar servidores SimpleX Chat? No comment provided by engineer. + + 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 chat Usar Chat @@ -6036,9 +8302,19 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Usar perfil actual No comment provided by engineer. + + 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 - Usar para conexiones nuevas + Para conexiones nuevas No comment provided by engineer. @@ -6061,24 +8337,54 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb ¿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 the app while in the call. Usar la aplicación durante la llamada. No comment provided by engineer. - - User profile - Perfil de usuario + + Use the app with one hand. + Usa la aplicación con una sola mano. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Usar hosts .onion requiere un proveedor VPN compatible. + + 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. @@ -6143,7 +8449,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb 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 compruébalo más tarde. + 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. @@ -6151,11 +8457,21 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb 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 + No comment provided by engineer. + Visible history Historial visible @@ -6171,8 +8487,8 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Los mensajes de voz no están permitidos en este chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Los mensajes de voz no están permitidos en este grupo. No comment provided by engineer. @@ -6211,6 +8527,16 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb 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. + 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 @@ -6256,9 +8582,9 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Al iniciar llamadas de audio y vídeo. 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 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. @@ -6296,24 +8622,39 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb 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. + No comment provided by engineer. + + + 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: %@. + alert message + Wrong database passphrase Contraseña de base de datos incorrecta No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada. + snd 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. + file error text + Wrong passphrase! ¡Contraseña incorrecta! No comment provided by engineer. - - XFTP servers - Servidores XFTP - No comment provided by engineer. - - - You - + + XFTP server + Servidor XFTP No comment provided by engineer. @@ -6338,7 +8679,12 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb You are already connected to %@. - Ya estás conectado a %@. + 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. @@ -6388,11 +8734,26 @@ Repeat join request? 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 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 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 @@ -6423,11 +8784,21 @@ Repeat join request? Puedes hacerlo visible para tus contactos de SimpleX en Configuración. No comment provided by engineer. - - You can now send messages to %@ - Ya puedes enviar mensajes a %@ + + You can now chat with %@ + Ya puedes chatear con %@ notification body + + 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. @@ -6443,16 +8814,16 @@ Repeat join request? 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 código QR para que cualquiera pueda conectarse contigo. - 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 No comment provided by engineer. + + 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. @@ -6466,23 +8837,23 @@ Repeat join request? 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. + alert message You can't send messages! ¡No puedes enviar mensajes! 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 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! @@ -6495,11 +8866,6 @@ Repeat connection request? ¿Repetir solicitud? No comment provided by engineer. - - You have no chats - No tienes chats - 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. @@ -6520,11 +8886,26 @@ Repeat connection request? Te has unido a este grupo. Conectando con el emisor de la invitacíon. 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. @@ -6540,29 +8921,34 @@ Repeat connection request? Has enviado una invitación de grupo No comment provided by engineer. + + You should receive notifications. + Deberías recibir notificaciones. + token info + 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. + 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 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 compruébalo más tarde. + 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 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. + 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 compruébalo más tarde. + 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á identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano. + Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano. No comment provided by engineer. @@ -6575,6 +8961,11 @@ Repeat connection request? Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. No comment provided by engineer. + + You will stop receiving messages from this 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á. @@ -6595,31 +8986,16 @@ Repeat connection request? 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 %@ servers - Mis servidores %@ - No comment provided by engineer. - Your ICE servers Servidores ICE No comment provided by engineer. - - Your SMP servers - Servidores SMP - No comment provided by engineer. - Your SimpleX address Mi dirección SimpleX No comment provided by engineer. - - Your XFTP servers - Servidores XFTP - No comment provided by engineer. - Your calls Llamadas @@ -6635,16 +9011,19 @@ Repeat connection request? La base de datos no está cifrada - establece una contraseña para cifrarla. No comment provided by engineer. + + Your chat preferences + Tus preferencias de chat + alert title + Your chat profiles Mis perfiles No comment provided by engineer. - - 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). - El contacto debe estar en línea para completar la conexión. -Puedes cancelarla 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. @@ -6662,6 +9041,11 @@ Puedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace Tus contactos permanecerán conectados. No comment provided by engineer. + + 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. @@ -6689,34 +9073,37 @@ Puedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace Your profile **%@** will be shared. - Tu perfil **%@** será compartido. + 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 se almacena en tu dispositivo y sólo se comparte con tus contactos. -Los servidores de SimpleX no pueden ver tu perfil. + + 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. - - Your profile, contacts and delivered messages are stored on your device. - Tu perfil, contactos y mensajes se almacenan en tu dispositivo. + + 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. No comment provided by engineer. + + 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. + alert message + Your random profile Tu perfil aleatorio No comment provided by engineer. - - Your server - Tu servidor - No comment provided by engineer. - Your server address - Dirección de tu servidor + Dirección del servidor + No comment provided by engineer. + + + Your servers + Tus servidores No comment provided by engineer. @@ -6759,6 +9146,11 @@ Los servidores de SimpleX no pueden ver tu perfil. llamada aceptada call status + + accepted invitation + invitación aceptada + chat list item title + admin administrador @@ -6794,6 +9186,16 @@ Los servidores de SimpleX no pueden ver tu perfil. y %lld evento(s) más No comment provided by engineer. + + archived report + informes archivados + No comment provided by engineer. + + + attempts + intentos + No comment provided by engineer. + audio call (not e2e encrypted) llamada (sin cifrar) @@ -6827,13 +9229,19 @@ Los servidores de SimpleX no pueden ver tu perfil. blocked by admin bloqueado por administrador - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold negrita No comment provided by engineer. + + call + llamada + No comment provided by engineer. + call error error en llamada @@ -6906,7 +9314,7 @@ Los servidores de SimpleX no pueden ver tu perfil. connecting - conectando + conectando... No comment provided by engineer. @@ -6937,7 +9345,7 @@ Los servidores de SimpleX no pueden ver tu perfil. connecting… conectando… - chat list item title + No comment provided by engineer. connection established @@ -6984,19 +9392,25 @@ Los servidores de SimpleX no pueden ver tu perfil. días time unit + + decryption errors + errores de descifrado + No comment provided by engineer. + default (%@) - por defecto (%@) - pref value + predeterminado (%@) + delete after time +pref value default (no) - por defecto (no) + predeterminado (no) No comment provided by engineer. default (yes) - por defecto (sí) + predeterminado (sí) No comment provided by engineer. @@ -7011,7 +9425,7 @@ Los servidores de SimpleX no pueden ver tu perfil. deleted group - grupo eliminado + ha eliminado el grupo rcv group event chat item @@ -7034,6 +9448,11 @@ Los servidores de SimpleX no pueden ver tu perfil. mensaje duplicado integrity error chat item + + duplicates + duplicados + No comment provided by engineer. + e2e encrypted cifrado de extremo a extremo @@ -7109,9 +9528,9 @@ Los servidores de SimpleX no pueden ver tu perfil. error No comment provided by engineer. - - event happened - evento ocurrido + + expired + expirados No comment provided by engineer. @@ -7144,6 +9563,11 @@ Los servidores de SimpleX no pueden ver tu perfil. 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. + + inactive + inactivo + No comment provided by engineer. + incognito via contact address link en modo incógnito mediante enlace de dirección del contacto @@ -7184,6 +9608,11 @@ Los servidores de SimpleX no pueden ver tu perfil. invitación al grupo %@ group name + + invite + Invitar + No comment provided by engineer. + invited ha sido invitado @@ -7239,6 +9668,11 @@ Los servidores de SimpleX no pueden ver tu perfil. conectado rcv group event chat item + + message + mensaje + No comment provided by engineer. + message received mensaje recibido @@ -7264,6 +9698,11 @@ Los servidores de SimpleX no pueden ver tu perfil. moderado por %@ marked deleted chat item preview text + + moderator + moderador + member role + months meses @@ -7272,7 +9711,7 @@ Los servidores de SimpleX no pueden ver tu perfil. never nunca - No comment provided by engineer. + delete after time new message @@ -7303,8 +9742,8 @@ Los servidores de SimpleX no pueden ver tu perfil. off desactivado enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7321,6 +9760,16 @@ Los servidores de SimpleX no pueden ver tu perfil. Activado group pref value + + other + otros + No comment provided by engineer. + + + other errors + otros errores + No comment provided by engineer. + owner propietario @@ -7336,6 +9785,16 @@ Los servidores de SimpleX no pueden ver tu perfil. p2p No comment provided by engineer. + + pending + pendiente + No comment provided by engineer. + + + pending approval + pendiente de aprobación + No comment provided by engineer. + quantum resistant e2e encryption cifrado e2e resistente a tecnología cuántica @@ -7351,6 +9810,11 @@ Los servidores de SimpleX no pueden ver tu perfil. confirmación recibida… No comment provided by engineer. + + rejected + rechazado + No comment provided by engineer. + rejected call llamada rechazada @@ -7373,7 +9837,7 @@ Los servidores de SimpleX no pueden ver tu perfil. removed profile picture - imagen de perfil eliminada + ha eliminado la imagen del perfil profile update event chat item @@ -7381,6 +9845,11 @@ Los servidores de SimpleX no pueden ver tu perfil. te ha expulsado rcv group event chat item + + requested to connect + solicitado para conectar + chat list item title + saved guardado @@ -7391,6 +9860,11 @@ Los servidores de SimpleX no pueden ver tu perfil. Guardado desde %@ No comment provided by engineer. + + search + buscar + No comment provided by engineer. + sec seg @@ -7416,6 +9890,15 @@ Los servidores de SimpleX no pueden ver tu perfil. Enviar mensaje directo No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + información cola del servidor: %1$@ + +último mensaje recibido: %2$@ + queue info + set new contact address nueva dirección de contacto @@ -7423,7 +9906,7 @@ Los servidores de SimpleX no pueden ver tu perfil. set new profile picture - nueva imagen de perfil + tiene nueva imagen del perfil profile update event chat item @@ -7456,11 +9939,21 @@ Los servidores de SimpleX no pueden ver tu perfil. desconocido connection info + + unknown servers + con servidores desconocidos + No comment provided by engineer. + unknown status estado desconocido No comment provided by engineer. + + unprotected + con IP desprotegida + No comment provided by engineer. + updated group profile ha actualizado el perfil del grupo @@ -7501,6 +9994,11 @@ Los servidores de SimpleX no pueden ver tu perfil. mediante retransmisor No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) videollamada (sin cifrar) @@ -7526,6 +10024,11 @@ Los servidores de SimpleX no pueden ver tu perfil. semanas time unit + + when IP hidden + con IP oculta + No comment provided by engineer. + yes @@ -7610,7 +10113,7 @@ Los servidores de SimpleX no pueden ver tu perfil.
- +
@@ -7647,7 +10150,7 @@ Los servidores de SimpleX no pueden ver tu perfil.
- +
@@ -7667,4 +10170,250 @@ Los servidores de SimpleX no pueden ver tu perfil.
+ +
+ +
+ + + %d new events + %d evento(s) nuevo(s) + notification body + + + From %d chat(s) + De %d chat(s) + notification body + + + From: %@ + De: %@ + notification body + + + New events + Eventos nuevos + notification + + + New messages + Mensajes nuevos + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Todos los derechos reservados. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 a239bebbcf..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 @@
- +
- - - - - - 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) @@ -124,9 +97,12 @@ %@ on vahvistettu No comment provided by engineer. + + %@ server + No comment provided by engineer. + %@ servers - %@ palvelimet No comment provided by engineer. @@ -138,6 +114,10 @@ %@ haluaa muodostaa yhteyden! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members No comment provided by engineer. @@ -157,11 +137,31 @@ %d päivää time interval + + %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. + forward confirmation reason + %d hours %d tuntia time interval + + %d messages not forwarded + alert title + %d min %d min @@ -177,6 +177,10 @@ %d sek time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d ohitettua viestiä @@ -242,11 +246,6 @@ %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 @@ -297,11 +296,6 @@ %u viestit ohitettu. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) No comment provided by engineer. @@ -310,31 +304,21 @@ (this device v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille. + + **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. **Create group**: to create a new group. 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. + + **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). + + **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. @@ -347,11 +331,15 @@ **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. + + **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. + + **Scan / Paste link**: to connect via a link you received. + 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. @@ -376,11 +364,6 @@ \*bold* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -411,11 +394,6 @@ - historian muokkaaminen. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec time to disappear @@ -428,7 +406,8 @@ 1 day 1 päivä - time interval + delete after time +time interval 1 hour @@ -443,12 +422,26 @@ 1 month 1 kuukausi - time interval + delete after time +time interval 1 week 1 viikko - time interval + delete after time +time interval + + + 1 year + delete after time + + + 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 @@ -465,11 +458,6 @@ 30 sekuntia No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -519,31 +507,29 @@ Keskeytä osoitteenvaihto? No comment provided by engineer. - - About SimpleX - Tietoja SimpleX:stä - No comment provided by engineer. - About SimpleX Chat Tietoja SimpleX Chatistä No comment provided by engineer. - - About SimpleX address - Tietoja SimpleX osoitteesta + + About operators No comment provided by engineer. - - Accent color - Korostusväri + + Accent No comment provided by engineer. Accept Hyväksy accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + No comment provided by engineer. Accept connection request? @@ -558,20 +544,40 @@ Accept incognito Hyväksy tuntematon - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + No comment provided by engineer. + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active + token status text + + + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi. No comment provided by engineer. - - Add contact + + Add friends No comment provided by engineer. - - Add preset servers - Lisää esiasetettuja palvelimia + + Add list No comment provided by engineer. @@ -579,14 +585,18 @@ Lisää profiili No comment provided by engineer. + + Add server + Lisää palvelin + No comment provided by engineer. + Add servers by scanning QR codes. Lisää palvelimia skannaamalla QR-koodeja. No comment provided by engineer. - - Add server… - Lisää palvelin… + + Add team members No comment provided by engineer. @@ -594,11 +604,39 @@ Lisää toiseen laitteeseen No comment provided by engineer. + + Add to list + No comment provided by engineer. + Add welcome message Lisää tervetuloviesti 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 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address Osoite @@ -609,6 +647,14 @@ Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. No comment provided by engineer. @@ -623,6 +669,14 @@ Verkon lisäasetukset No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + + + All + No comment provided by engineer. + All app data is deleted. Kaikki sovelluksen tiedot poistetaan. @@ -633,16 +687,28 @@ Kaikki keskustelut ja viestit poistetaan - tätä ei voi kumota! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + alert message + All data is erased when it is entered. Kaikki tiedot poistetaan, kun se syötetään. No comment provided by engineer. + + All data is kept private on your device. + No comment provided by engineer. + All group members will remain connected. Kaikki ryhmän jäsenet pysyvät yhteydessä. No comment provided by engineer. + + All messages 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. @@ -656,6 +722,18 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + profile dropdown + + + 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. Kaikki kontaktisi pysyvät yhteydessä. @@ -680,11 +758,19 @@ Salli puhelut vain, jos kontaktisi sallii ne. No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Salli katoavat viestit vain, jos kontaktisi sallii sen sinulle. No comment provided by engineer. + + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia) @@ -710,11 +796,19 @@ Salli katoavien viestien lähettäminen. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia) No comment provided by engineer. + + Allow to report messsages to moderators. + No comment provided by engineer. + Allow to send SimpleX links. No comment provided by engineer. @@ -777,6 +871,10 @@ Already joining the group! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay Käytä aina relettä @@ -787,11 +885,20 @@ Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti. No comment provided by engineer. + + Another reason + report reason + 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: %@ @@ -805,6 +912,10 @@ App encrypts new local files (except videos). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon Sovelluksen kuvake @@ -820,6 +931,10 @@ Sovelluksen pääsykoodi korvataan itsetuhoutuvalla pääsykoodilla. No comment provided by engineer. + + App session + No comment provided by engineer. + App version Sovellusversio @@ -839,10 +954,46 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + + + Archive + No comment provided by engineer. + + + Archive %lld reports? + 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? + No comment provided by engineer. + + + Archive reports + swipe action + + + Archived contacts + No comment provided by engineer. + Archiving database No comment provided by engineer. @@ -907,11 +1058,19 @@ Hyväksy kuvat automaattisesti No comment provided by engineer. + + Auto-accept settings + alert title + Back Takaisin No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -926,15 +1085,51 @@ Virheellinen viestin tarkiste 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 Parempia viestejä 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. @@ -963,6 +1158,14 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Sekä sinä että kontaktisi voivat käyttää viestireaktioita. @@ -992,11 +1195,29 @@ Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + No comment provided by engineer. + + + 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). Chat-profiilin mukaan (oletus) tai [yhteyden mukaan](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! Puhelu on jo päättynyt! @@ -1007,10 +1228,22 @@ Puhelut No comment provided by engineer. + + 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! Kontaktia ei voi kutsua! @@ -1021,10 +1254,15 @@ Kontakteja ei voi kutsua! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel Peruuta - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1035,10 +1273,18 @@ Ei pääsyä avainnippuun tietokannan salasanan tallentamiseksi No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Tiedostoa ei voi vastaanottaa - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text Cellular @@ -1049,6 +1295,14 @@ Muuta No comment provided by engineer. + + Change automatic message deletion? + alert title + + + Change chat profiles + authentication reason + Change database passphrase? Muutetaanko tietokannan tunnuslause? @@ -1093,11 +1347,22 @@ Change self-destruct passcode Vaihda itsetuhoutuva pääsykoodi authentication reason - set passcode view +set passcode view - - Chat archive - Chat-arkisto + + 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. @@ -1115,6 +1380,10 @@ Chat-tietokanta poistettu No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported Chat-tietokanta tuotu @@ -1134,6 +1403,10 @@ 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. @@ -1143,15 +1416,44 @@ Chat-asetukset No comment provided by engineer. + + Chat preferences were changed. + alert message + + + Chat profile + Käyttäjäprofiili + 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 Keskustelut No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Tarkista palvelimen osoite ja yritä uudelleen. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1172,10 +1474,22 @@ Valitse kirjastosta No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear Tyhjennä - No comment provided by engineer. + swipe action Clear conversation @@ -1187,6 +1501,14 @@ Tyhjennä keskustelu? 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. @@ -1196,11 +1518,18 @@ Tyhjennä vahvistus No comment provided by engineer. - - Colors - Värit + + Color chats with the new themes. No comment provided by engineer. + + Color mode + No comment provided by engineer. + + + Community guidelines violation + report reason + Compare file Vertaa tiedostoa @@ -1211,11 +1540,47 @@ Vertaa turvakoodeja kontaktiesi kanssa. 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 Määritä ICE-palvelimet No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Vahvista @@ -1226,11 +1591,19 @@ Vahvista pääsykoodi No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades Vahvista tietokannan päivitykset No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -1253,6 +1626,10 @@ Confirm upload No comment provided by engineer. + + Confirmed + token status text + Connect Yhdistä @@ -1271,6 +1648,10 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? No comment provided by engineer. @@ -1303,14 +1684,26 @@ This is your own one-time link! Connect with %@ 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 to server… Yhteyden muodostaminen palvelimeen… @@ -1321,6 +1714,10 @@ This is your own one-time link! Yhteyden muodostaminen palvelimeen... (virhe: %@) 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. @@ -1330,6 +1727,14 @@ This is your own one-time link! Yhteys No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + + + Connection blocked + No comment provided by engineer. + Connection error Yhteysvirhe @@ -1340,11 +1745,32 @@ This is your own one-time link! Yhteysvirhe (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + No comment provided by engineer. + + + Connection not ready. + No comment provided by engineer. + + + Connection notifications + No comment provided by engineer. + Connection request sent! Yhteyspyyntö lähetetty! 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. @@ -1354,6 +1780,14 @@ This is your own one-time link! Yhteyden aikakatkaisu No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows Kontakti sallii @@ -1364,6 +1798,10 @@ This is your own one-time link! Kontakti on jo olemassa No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: Kontakti piilotettu: @@ -1374,9 +1812,8 @@ This is your own one-time link! Kontakti on yhdistetty notification - - Contact is not connected yet! - Kontaktia ei ole vielä yhdistetty! + + Contact is deleted. No comment provided by engineer. @@ -1389,6 +1826,10 @@ This is your own one-time link! Kontaktin asetukset No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Kontaktit @@ -1399,21 +1840,37 @@ This is your own one-time link! Kontaktit voivat merkitä viestit poistettaviksi; voit katsella niitä. No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue Jatka No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy Kopioi - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ Ydinversio: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? No comment provided by engineer. @@ -1423,6 +1880,10 @@ This is your own one-time link! Luo No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Luo SimpleX-osoite @@ -1432,11 +1893,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä. - No comment provided by engineer. - Create file Luo tiedosto @@ -1456,6 +1912,10 @@ This is your own one-time link! Luo linkki No comment provided by engineer. + + Create list + No comment provided by engineer. + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 Luo uusi profiili [työpöytäsovelluksessa](https://simplex.chat/downloads/). 💻 @@ -1463,6 +1923,7 @@ This is your own one-time link! Create profile + Luo profiilisi No comment provided by engineer. @@ -1480,6 +1941,10 @@ This is your own one-time link! Luo profiilisi No comment provided by engineer. + + Created + No comment provided by engineer. + Created at No comment provided by engineer. @@ -1488,11 +1953,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - Luotu %@ - No comment provided by engineer. - Creating archive link No comment provided by engineer. @@ -1506,11 +1966,19 @@ This is your own one-time link! Nykyinen pääsykoodi 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 passphrase… Nykyinen tunnuslause… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. Nykyinen tuettu enimmäistiedostokoko on %@. @@ -1521,11 +1989,23 @@ This is your own one-time link! Mukautettu aika No comment provided by engineer. + + Customizable message shape. + No comment provided by engineer. + + + Customize theme + No comment provided by engineer. + Dark Tumma No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID Tietokannan tunnus @@ -1624,6 +2104,10 @@ This is your own one-time link! Tietokanta siirretään, kun sovellus käynnistyy uudelleen No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized Hajautettu @@ -1637,17 +2121,17 @@ This is your own one-time link! Delete Poista - chat item action + alert action +swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - Poista kontakti - No comment provided by engineer. - Delete address Poista osoite @@ -1672,14 +2156,12 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. - - Delete archive - Poista arkisto + + Delete chat No comment provided by engineer. - - Delete chat archive? - Poista keskusteluarkisto? + + Delete chat messages from your device. No comment provided by engineer. @@ -1692,6 +2174,10 @@ This is your own one-time link! Poista keskusteluprofiili? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Poista yhteys @@ -1702,9 +2188,8 @@ This is your own one-time link! Poista kontakti No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1766,6 +2251,10 @@ This cannot be undone! Poista linkki? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Poista jäsenviesti? @@ -1779,7 +2268,7 @@ This cannot be undone! Delete messages Poista viestit - No comment provided by engineer. + alert button Delete messages after @@ -1796,9 +2285,8 @@ This cannot be undone! Poista vanha tietokanta? No comment provided by engineer. - - Delete pending connection - Poista vireillä oleva yhteys + + Delete or moderate up to 200 messages. No comment provided by engineer. @@ -1816,11 +2304,27 @@ This cannot be undone! Poista jono server test step + + Delete report + No comment provided by engineer. + + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Poista käyttäjäprofiili? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at Poistettu klo @@ -1831,6 +2335,14 @@ This cannot be undone! Poistettu klo: %@ copied message info + + Deletion errors + No comment provided by engineer. + + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Toimitus @@ -1863,11 +2375,35 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + 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 Kehitä No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools Kehittäjätyökalut @@ -1898,8 +2434,12 @@ This cannot be undone! Yksityisviestit chat feature - - 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. Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. @@ -1913,11 +2453,23 @@ This cannot be undone! Poista SimpleX Lock käytöstä authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Poista käytöstä kaikilta No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Tuhoutuva viesti @@ -1933,8 +2485,8 @@ This cannot be undone! Katoavat viestit ovat kiellettyjä tässä keskustelussa. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Katoavat viestit ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. @@ -1966,11 +2518,19 @@ This cannot be undone! Discover via local network 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 SimpleX for emergency calls. Älä käytä SimpleX-sovellusta hätäpuheluihin. No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later Tee myöhemmin @@ -1980,6 +2540,14 @@ This cannot be undone! Do not send history to new members. No comment provided by engineer. + + Do not use credentials with proxy. + No comment provided by engineer. + + + Documents: + No comment provided by engineer. + Don't create address Älä luo osoitetta @@ -1990,11 +2558,19 @@ This cannot be undone! Älä salli No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Älä näytä uudelleen No comment provided by engineer. + + Done + No comment provided by engineer. + Downgrade and open chat Alenna ja avaa keskustelu @@ -2002,7 +2578,12 @@ This cannot be undone! Download - chat item action + alert button +chat item action + + + Download errors + No comment provided by engineer. Download failed @@ -2013,6 +2594,18 @@ This cannot be undone! Lataa tiedosto server test step + + Download files + alert action + + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive No comment provided by engineer. @@ -2031,6 +2624,10 @@ This cannot be undone! Kesto No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Muokkaa @@ -2051,6 +2648,10 @@ This cannot be undone! Salli (pidä ohitukset) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock Ota SimpleX Lock käyttöön @@ -2064,7 +2665,7 @@ This cannot be undone! Enable automatic message deletion? Ota automaattinen viestien poisto käyttöön? - No comment provided by engineer. + alert title Enable camera access @@ -2109,6 +2710,10 @@ This cannot be undone! Ota itsetuhoava pääsykoodi käyttöön set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2179,6 +2784,10 @@ This cannot be undone! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Syötä pääsykoodi @@ -2240,26 +2849,33 @@ This cannot be undone! Virhe osoitteenmuutoksen keskeytyksessä No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Virhe kontaktipyynnön hyväksymisessä No comment provided by engineer. - - Error accessing database file - Virhe tietokantatiedoston käyttämisessä - No comment provided by engineer. - Error adding member(s) Virhe lisättäessä jäseniä No comment provided by engineer. + + Error adding server + alert title + Error changing address Virhe osoitteenvaihdossa No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Virhe roolin vaihdossa @@ -2270,6 +2886,18 @@ This cannot be undone! Virhe asetuksen muuttamisessa 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 Virhe osoitteen luomisessa @@ -2285,6 +2913,10 @@ This cannot be undone! Virhe ryhmälinkin luomisessa No comment provided by engineer. + + Error creating list + alert title + Error creating member contact No comment provided by engineer. @@ -2298,6 +2930,10 @@ This cannot be undone! Virhe profiilin luomisessa! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Virhe tiedoston salauksen purussa @@ -2318,11 +2954,6 @@ This cannot be undone! 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 @@ -2367,6 +2998,10 @@ This cannot be undone! Virhe vietäessä keskustelujen tietokantaa No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Virhe keskustelujen tietokannan tuonnissa @@ -2377,9 +3012,12 @@ This cannot be undone! Virhe ryhmään liittymisessä No comment provided by engineer. - - Error loading %@ servers - Virhe %@-palvelimien lataamisessa + + Error loading servers + alert title + + + Error migrating settings No comment provided by engineer. @@ -2389,16 +3027,31 @@ This cannot be undone! Error receiving file Virhe tiedoston vastaanottamisessa + alert title + + + Error reconnecting server No comment provided by engineer. + + Error reconnecting servers + No comment provided by engineer. + + + Error registering for notifications + alert title + Error removing member Virhe poistettaessa jäsentä No comment provided by engineer. - - Error saving %@ servers - Virhe %@ palvelimien tallentamisessa + + Error reordering lists + alert title + + + Error resetting statistics No comment provided by engineer. @@ -2406,6 +3059,10 @@ This cannot be undone! Virhe ICE-palvelimien tallentamisessa No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile Virhe ryhmäprofiilin tallentamisessa @@ -2421,6 +3078,10 @@ This cannot be undone! Virhe tunnuslauseen tallentamisessa avainnippuun No comment provided by engineer. + + Error saving servers + alert title + Error saving settings when migrating @@ -2463,16 +3124,24 @@ This cannot be undone! Virhe keskustelun lopettamisessa No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Virhe profiilin vaihdossa! - No comment provided by engineer. + alertTitle Error synchronizing connection Virhe yhteyden synkronoinnissa No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link Virhe ryhmälinkin päivittämisessä @@ -2483,6 +3152,10 @@ This cannot be undone! Virhe viestin päivityksessä No comment provided by engineer. + + Error updating server + alert title + Error updating settings Virhe asetusten päivittämisessä @@ -2509,7 +3182,9 @@ This cannot be undone! Error: %@ Virhe: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2521,6 +3196,14 @@ This cannot be undone! Virhe: ei tietokantatiedostoa No comment provided by engineer. + + Errors + No comment provided by engineer. + + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Jopa kun ei käytössä keskustelussa. @@ -2535,6 +3218,10 @@ This cannot be undone! Expand chat item action + + Expired + token status text + Export database Vie tietokanta @@ -2545,6 +3232,10 @@ This cannot be undone! Vientivirhe: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Viety tietokanta-arkisto. @@ -2569,15 +3260,57 @@ This cannot be undone! Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa! 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. + No comment provided by engineer. + Favorite Suosikki + swipe action + + + Favorites No comment provided by engineer. + + File error + file error alert title + + + File errors: +%@ + alert message + + + File is blocked by server operator: +%@. + file error text + + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. Tiedosto poistetaan palvelimilta. @@ -2598,6 +3331,10 @@ This cannot be undone! Tiedosto: %@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media Tiedostot & media @@ -2608,8 +3345,8 @@ This cannot be undone! Tiedostot ja media chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Tiedostot ja media ovat tässä ryhmässä kiellettyjä. No comment provided by engineer. @@ -2675,19 +3412,59 @@ This cannot be undone! Ryhmän jäsen ei tue korjausta No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + servers error + For console Konsoliin 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. + No comment provided by engineer. + + + For me + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward chat item action + + Forward %d message(s)? + alert title + Forward and save messages No comment provided by engineer. + + Forward messages + alert action + + + Forward messages without files? + alert message + + + Forward up to 20 messages at once. + No comment provided by engineer. + Forwarded No comment provided by engineer. @@ -2696,6 +3473,32 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding %lld messages + 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop No comment provided by engineer. @@ -2715,11 +3518,6 @@ This cannot be undone! Koko nimi (valinnainen) No comment provided by engineer. - - Full name: - Koko nimi: - No comment provided by engineer. - Fully decentralized – visible only to members. No comment provided by engineer. @@ -2739,6 +3537,18 @@ This cannot be undone! GIFit ja tarrat No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + message preview + + + Good morning! + message preview + Group Ryhmä @@ -2792,40 +3602,6 @@ This cannot be undone! 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. (24 hours) - Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia) - No comment provided by engineer. - - - Group members can send SimpleX links. - 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ä. - No comment provided by engineer. - Group message: Ryhmäviesti: @@ -2866,11 +3642,19 @@ This cannot be undone! Ryhmä poistetaan sinulta - tätä ei voi perua! No comment provided by engineer. + + Groups + No comment provided by engineer. + Help Apua No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Piilotettu @@ -2920,10 +3704,17 @@ This cannot be undone! Miten SimpleX toimii No comment provided by engineer. + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy + No comment provided by engineer. + How it works - Kuinka se toimii - No comment provided by engineer. + alert button How to @@ -2949,6 +3740,10 @@ This cannot be undone! ICE-palvelimet (yksi per rivi) No comment provided by engineer. + + IP address + 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. @@ -2989,8 +3784,8 @@ This cannot be undone! Heti No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Immuuni roskapostille ja väärinkäytöksille No comment provided by engineer. @@ -3013,10 +3808,19 @@ This cannot be undone! 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. +More improvements are coming soon! + No comment provided by engineer. + Improved message delivery No comment provided by engineer. @@ -3044,6 +3848,14 @@ This cannot be undone! In-call sounds No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Incognito @@ -3112,6 +3924,11 @@ This cannot be undone! Asenna [SimpleX Chat terminaalille](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Heti + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3119,16 +3936,35 @@ This cannot be undone! No comment provided by engineer. - - Instantly - Heti - No comment provided by engineer. - Interface Käyttöliittymä No comment provided by engineer. + + Interface colors + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code No comment provided by engineer. @@ -3161,7 +3997,7 @@ This cannot be undone! Invalid server address! Virheellinen palvelinosoite! - No comment provided by engineer. + alert title Invalid status @@ -3183,6 +4019,10 @@ This cannot be undone! Kutsu jäseniä No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Kutsu ryhmään @@ -3198,8 +4038,8 @@ This cannot be undone! Viestien peruuttamaton poisto on kielletty tässä keskustelussa. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Viestien peruuttamaton poisto on kielletty tässä ryhmässä. No comment provided by engineer. @@ -3224,6 +4064,10 @@ This cannot be undone! 3. Yhteys vaarantui. No comment provided by engineer. + + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Näyttäisi, että olet jo yhteydessä tämän linkin kautta. Jos näin ei ole, tapahtui virhe (%@). @@ -3242,7 +4086,7 @@ This cannot be undone! Join Liity - No comment provided by engineer. + swipe action Join group @@ -3278,6 +4122,10 @@ This is your link for group %@! Keep + alert action + + + Keep conversation No comment provided by engineer. @@ -3286,7 +4134,7 @@ This is your link for group %@! Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3321,6 +4169,14 @@ This is your link for group %@! Leave Poistu + swipe action + + + Leave chat + No comment provided by engineer. + + + Leave chat? No comment provided by engineer. @@ -3360,6 +4216,18 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! Live-viesti! @@ -3370,11 +4238,6 @@ This is your link for group %@! Live-viestit No comment provided by engineer. - - Local - Paikallinen - No comment provided by engineer. - Local name Paikallinen nimi @@ -3395,11 +4258,6 @@ This is your link for group %@! 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 @@ -3410,21 +4268,11 @@ This is your link for group %@! 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 @@ -3450,11 +4298,31 @@ This is your link for group %@! Enintään 30 sekuntia, vastaanotetaan välittömästi. No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member Jäsen No comment provided by engineer. + + Member inactive + item status text + + + Member reports + chat feature + + + 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. Jäsenen rooli muuttuu muotoon "%@". Kaikille ryhmän jäsenille ilmoitetaan asiasta. @@ -3465,11 +4333,61 @@ This is your link for group %@! Jäsenen rooli muutetaan muotoon "%@". Jäsen saa uuden kutsun. 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! 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 report messsages to moderators. + No comment provided by engineer. + + + Members can send SimpleX links. + 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ä. + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + No comment provided by engineer. + Message delivery error Viestin toimitusvirhe @@ -3480,11 +4398,27 @@ This is your link for group %@! Viestien toimituskuittaukset! No comment provided by engineer. + + Message delivery warning + item status text + Message draft Viestiluonnos No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions Viestireaktiot @@ -3495,15 +4429,35 @@ This is your link for group %@! Viestireaktiot ovat kiellettyjä tässä keskustelussa. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Viestireaktiot ovat kiellettyjä tässä ryhmässä. 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 + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text Viestin teksti @@ -3527,6 +4481,22 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + + + Messages were deleted after you selected them. + alert message + 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. @@ -3583,9 +4553,9 @@ This is your link for group %@! Siirto on valmis No comment provided by engineer. - - Migrations: %@ - Siirrot: %@ + + Migrations: + Siirrot: No comment provided by engineer. @@ -3603,6 +4573,10 @@ This is your link for group %@! Moderoitu klo: %@ copied message info + + More + swipe action + More improvements are coming soon! Lisää parannuksia on tulossa pian! @@ -3612,16 +4586,15 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Todennäköisesti tämä yhteys on poistettu. item status description - - 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 @@ -3630,7 +4603,11 @@ This is your link for group %@! Mute Mykistä - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3640,7 +4617,7 @@ This is your link for group %@! Name Nimi - No comment provided by engineer. + swipe action Network & servers @@ -3651,10 +4628,22 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + snd error text + Network management No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Verkkoasetukset @@ -3665,15 +4654,31 @@ This is your link for group %@! Verkon tila No comment provided by engineer. + + New + token status text + New Passcode Uusi pääsykoodi No comment provided by engineer. + + New SOCKS credentials will be used every time you start the app. + No comment provided by engineer. + + + New SOCKS credentials will be used for each server. + No comment provided by engineer. + New chat No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request Uusi kontaktipyyntö @@ -3684,11 +4689,6 @@ This is your link for group %@! Uusi kontakti: notification - - New database archive - Uusi tietokanta-arkisto - No comment provided by engineer. - New desktop app! No comment provided by engineer. @@ -3698,11 +4698,19 @@ This is your link for group %@! Uusi näyttönimi No comment provided by engineer. + + New events + notification + New in %@ Uutta %@ No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role Uusi jäsenrooli @@ -3718,6 +4726,10 @@ This is your link for group %@! Uusi tunnuslause… No comment provided by engineer. + + New server + No comment provided by engineer. + No Ei @@ -3728,6 +4740,18 @@ This is your link for group %@! Ei sovelluksen salasanaa Authentication unavailable + + 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 Kontakteja ei ole valittu @@ -3748,6 +4772,10 @@ This is your link for group %@! Ei laitetunnusta! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Ei suodatettuja keskusteluja @@ -3763,24 +4791,94 @@ This is your link for group %@! Ei historiaa No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + + + No media & file servers. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + servers error + 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 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 No comment provided by engineer. + + 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. + servers error + + + No token! + alert title + + + No unread chats + No comment provided by engineer. + + + No user identifiers. + Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi. + No comment provided by engineer. + Not compatible! No comment provided by engineer. + + Notes + No comment provided by engineer. + + + Nothing selected + No comment provided by engineer. + + + Nothing to forward! + alert title + Notifications Ilmoitukset @@ -3791,6 +4889,18 @@ This is your link for group %@! Ilmoitukset on poistettu käytöstä! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3807,36 +4917,35 @@ This is your link for group %@! Off Pois - No comment provided by engineer. + blur media Ok Ok - No comment provided by engineer. + alert button Old database Vanha tietokanta No comment provided by engineer. - - Old database archive - Vanha tietokanta-arkisto - No comment provided by engineer. - One-time invitation link Kertakutsulinkki No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Edellyttää VPN:n sallimista. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Yhteyden muodostamiseen tarvitaan Onion-isäntiä. +Edellyttää VPN:n sallimista. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion-isäntiä käytetään, kun niitä on saatavilla. Edellyttää VPN:n sallimista. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion-isäntiä käytetään, kun niitä on saatavilla. +Edellyttää VPN:n sallimista. No comment provided by engineer. @@ -3844,11 +4953,19 @@ This is your link for group %@! 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**. + + Only chat owners can change preferences. + No comment provided by engineer. + + + 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 delete conversation + No comment provided by engineer. + Only group owners can change group preferences. Vain ryhmän omistajat voivat muuttaa ryhmän asetuksia. @@ -3864,6 +4981,14 @@ This is your link for group %@! Vain ryhmän omistajat voivat ottaa ääniviestit käyttöön. 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. Vain sinä voit lisätä viestireaktioita. @@ -3916,13 +5041,17 @@ This is your link for group %@! Open - No comment provided by engineer. + alert action Open Settings Avaa Asetukset No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Avaa keskustelu @@ -3933,28 +5062,38 @@ This is your link for group %@! Avaa keskustelukonsoli authentication reason + + Open conditions + No comment provided by engineer. + Open group No comment provided by engineer. + + Open link? + alert title + Open migration to another device authentication reason - - Open user profiles - Avaa käyttäjäprofiilit - authentication reason - - - 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 app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + + + Or import archive file + No comment provided by engineer. + Or paste archive link No comment provided by engineer. @@ -3971,10 +5110,23 @@ This is your link for group %@! 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 file errors: +%@ + alert message + PING count PING-määrä @@ -4010,6 +5162,10 @@ This is your link for group %@! Pääsykoodi asetettu! No comment provided by engineer. + + Password + No comment provided by engineer. + Password to show Salasana näytettäväksi @@ -4036,13 +5192,12 @@ This is your link for group %@! Paste the link you received 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. + + Pending No comment provided by engineer. - - Periodically + + Periodic Ajoittain No comment provided by engineer. @@ -4055,11 +5210,24 @@ This is your link for group %@! Picture-in-picture calls 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. Pyydä kontaktiasi sallimaan ääniviestien lähettäminen. 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. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki. @@ -4124,11 +5292,27 @@ Error: %@ Säilytä tunnuslause turvallisesti, ET voi muuttaa sitä, jos kadotat sen. No comment provided by engineer. + + 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. + token info + Polish interface Puolalainen käyttöliittymä No comment provided by engineer. + + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen @@ -4139,40 +5323,75 @@ Error: %@ Säilytä viimeinen viestiluonnos liitteineen. No comment provided by engineer. - - Preset server - Esiasetettu palvelin - No comment provided by engineer. - Preset server address Esiasetettu palvelimen osoite No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Esikatselu No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Yksityisyys ja turvallisuus 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 Yksityisyys uudelleen määritettynä No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + No comment provided by engineer. + Private filenames Yksityiset tiedostonimet 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 🚀 + No comment provided by engineer. + Private notes name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections Profiili- ja palvelinyhteydet @@ -4187,23 +5406,19 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - No comment provided by engineer. - Profile password Profiilin salasana No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Profiilipäivitys lähetetään kontakteillesi. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4225,6 +5440,10 @@ Error: %@ Estä viestireaktiot. No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. No comment provided by engineer. @@ -4249,11 +5468,20 @@ Error: %@ Estä ääniviestien lähettäminen. No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen Suojaa sovellusnäyttö No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Suojaa keskusteluprofiilisi salasanalla! @@ -4269,6 +5497,18 @@ Error: %@ Protokollan aikakatkaisu per 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 Push-ilmoitukset @@ -4287,6 +5527,10 @@ Error: %@ Arvioi sovellus No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… Reagoi… @@ -4295,32 +5539,27 @@ Error: %@ Read Lue - No comment provided by engineer. + swipe action Read more Lue lisää No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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). + Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Lue lisää GitHub-tietovarastostamme. - 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). @@ -4331,6 +5570,10 @@ Error: %@ Kuittaukset pois käytöstä No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Vastaanotettu klo @@ -4351,15 +5594,23 @@ Error: %@ Vastaanotettu viesti message info title + + 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. Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon. No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. Tiedoston vastaanotto pysäytetään. @@ -4383,11 +5634,31 @@ Error: %@ Vastaanottajat näkevät päivitykset, kun kirjoitat niitä. No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Yhdistä kaikki yhdistetyt palvelimet uudelleen pakottaaksesi viestin toimituksen. Tämä käyttää ylimääräistä liikennettä. No comment provided by engineer. + + Reconnect 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? Yhdistä palvelimet uudelleen? @@ -4408,10 +5679,23 @@ Error: %@ Pienempi akun käyttö No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject Hylkää - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4438,6 +5722,14 @@ Error: %@ Poista No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + + + Remove image + No comment provided by engineer. + Remove member Poista jäsen @@ -4493,6 +5785,46 @@ Error: %@ Vastaa chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required Pakollinen @@ -4503,16 +5835,36 @@ Error: %@ Oletustilaan 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 Oletusvärit No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Palauta oletusasetukset No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Käynnistä sovellus uudelleen uuden keskusteluprofiilin luomiseksi @@ -4552,9 +5904,8 @@ Error: %@ Paljasta chat item action - - Revert - Palauta + + Review conditions No comment provided by engineer. @@ -4582,9 +5933,16 @@ Error: %@ Käynnistä chat No comment provided by engineer. - - SMP servers - SMP-palvelimet + + SMP server + No comment provided by engineer. + + + SOCKS proxy + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4594,43 +5952,42 @@ Error: %@ Save Tallenna - chat item action + alert button +chat item action 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. + alert button Save and notify group members Tallenna ja ilmoita ryhmän jäsenille No comment provided by engineer. + + Save and reconnect + No comment provided by engineer. + Save and update group profile Tallenna ja päivitä ryhmäprofiili No comment provided by engineer. - - Save archive - Tallenna arkisto - No comment provided by engineer. - - - Save auto-accept settings - Tallenna automaattisen hyväksynnän asetukset - No comment provided by engineer. - Save group profile Tallenna ryhmäprofiili No comment provided by engineer. + + Save list + No comment provided by engineer. + Save passphrase and open chat Tallenna tunnuslause ja avaa keskustelu @@ -4644,7 +6001,7 @@ Error: %@ Save preferences? Tallenna asetukset? - No comment provided by engineer. + alert title Save profile password @@ -4659,18 +6016,17 @@ Error: %@ Save servers? Tallenna palvelimet? - No comment provided by engineer. - - - Save settings? - Tallenna asetukset? - No comment provided by engineer. + alert title Save welcome message? Tallenna tervetuloviesti? No comment provided by engineer. + + Save your profile? + alert title + Saved No comment provided by engineer. @@ -4688,6 +6044,18 @@ Error: %@ Saved message message info title + + Saving %lld messages + No comment provided by engineer. + + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Skannaa QR-koodi @@ -4725,11 +6093,19 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Turvallinen jono server test step + + Secured + No comment provided by engineer. + Security assessment Turvallisuusarviointi @@ -4743,6 +6119,18 @@ Error: %@ Select Valitse + chat item action + + + Select chat profile + No comment provided by engineer. + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4780,11 +6168,6 @@ Error: %@ Lähetä toimituskuittaukset vastaanottajalle No comment provided by engineer. - - Send direct message - Lähetä yksityisviesti - No comment provided by engineer. - Send direct message to connect No comment provided by engineer. @@ -4794,6 +6177,10 @@ Error: %@ Lähetä katoava viesti No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Lähetä linkkien esikatselu @@ -4804,14 +6191,25 @@ Error: %@ Lähetä live-viesti 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. + 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 Lähetys ilmoitukset No comment provided by engineer. - - Send notifications: - Lähetys ilmoitukset: + + Send private reports No comment provided by engineer. @@ -4836,7 +6234,7 @@ Error: %@ Sender cancelled file transfer. Lähettäjä peruutti tiedoston siirron. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -4893,6 +6291,10 @@ Error: %@ Lähetetty klo: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Lähetetty tiedosto tapahtuma @@ -4903,11 +6305,59 @@ Error: %@ Lähetetty viesti message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. Lähetetyt viestit poistetaan asetetun ajan kuluttua. 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 + No comment provided by engineer. + + + Server added to operator %@. + alert message + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana @@ -4923,11 +6373,31 @@ Error: %@ Palvelintesti epäonnistui! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers Palvelimet 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. @@ -4937,11 +6407,19 @@ Error: %@ Aseta 1 päivä No comment provided by engineer. + + Set chat name… + No comment provided by engineer. + Set contact name… Aseta kontaktin nimi… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Aseta ryhmän asetukset @@ -4952,6 +6430,10 @@ Error: %@ Aseta se järjestelmän todennuksen sijaan. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Aseta pääsykoodi @@ -4981,6 +6463,10 @@ Error: %@ Asetukset No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images No comment provided by engineer. @@ -4988,21 +6474,38 @@ Error: %@ Share Jaa - chat item action + alert action +chat item action Share 1-time link Jaa kertakäyttölinkki No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Jaa osoite No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Jaa osoite kontakteille? + alert title + + + Share from other apps. No comment provided by engineer. @@ -5010,15 +6513,27 @@ Error: %@ Jaa linkki No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Jaa kontaktien kanssa No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -5038,21 +6553,41 @@ Error: %@ Näytä viimeiset viestit No comment provided by engineer. + + Show message status + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview Näytä esikatselu No comment provided by engineer. + + Show → on messages sent via private routing. + No comment provided by engineer. + Show: Näytä: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX-osoite No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Trail of Bits on tarkastanut SimpleX Chatin tietoturvan. @@ -5083,6 +6618,18 @@ Error: %@ SimpleX-osoite No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX-yhteystiedot @@ -5103,8 +6650,8 @@ Error: %@ SimpleX-linkit chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. No comment provided by engineer. @@ -5116,10 +6663,18 @@ Error: %@ SimpleX-kertakutsu simplex link type + + SimpleX protocols reviewed by Trail of Bits. + No comment provided by engineer. + Simplified incognito mode No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Ohita @@ -5135,16 +6690,42 @@ Error: %@ Pienryhmät (max 20) No comment provided by engineer. + + Soft + blur media + + + 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. Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja. No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + + + Some servers failed the test: +%@ + alert message + Somebody Joku notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. No comment provided by engineer. @@ -5163,6 +6744,14 @@ Error: %@ Aloita siirto No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Lopeta @@ -5177,11 +6766,6 @@ Error: %@ Stop chat 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. @@ -5210,27 +6794,55 @@ Error: %@ Stop sharing Lopeta jakaminen - No comment provided by engineer. + alert action Stop sharing address? Lopeta osoitteen jakaminen? - No comment provided by engineer. + alert title Stopping chat No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + blur media + Submit Lähetä 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 tuki 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 Järjestelmä @@ -5241,11 +6853,19 @@ Error: %@ Järjestelmän todennus No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout TCP-yhteyden aikakatkaisu No comment provided by engineer. + + TCP port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5261,11 +6881,19 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Ota kuva No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Napauta painiketta @@ -5298,16 +6926,19 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - Aloita uusi keskustelu napauttamalla - No comment provided by engineer. + + Temporary file error + file error alert title Test failed at step %@. Testi epäonnistui vaiheessa %@. server test failure + + Test notifications + No comment provided by engineer. + Test server Testipalvelin @@ -5321,7 +6952,7 @@ Error: %@ Tests failed! Testit epäonnistuivat! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5338,11 +6969,6 @@ Error: %@ 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 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. @@ -5355,6 +6981,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön. 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). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun. @@ -5364,6 +6998,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Hyväksymäsi yhteys peruuntuu! @@ -5384,6 +7022,11 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin! No comment provided by engineer. + + The future of messaging + Seuraavan sukupolven yksityisviestit + No comment provided by engineer. + The hash of the previous message is different. Edellisen viestin tarkiste on erilainen. @@ -5399,9 +7042,12 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Viesti merkitään moderoiduksi kaikille jäsenille. No comment provided by engineer. - - The next generation of private messaging - Seuraavan sukupolven yksityisviestit + + 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. @@ -5409,9 +7055,12 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa. No comment provided by engineer. - - The profile is only shared with your contacts. - Profiili jaetaan vain kontaktiesi kanssa. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! No comment provided by engineer. @@ -5429,13 +7078,24 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - Teema + + 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. @@ -5458,6 +7118,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Tämä voi kestää useita minuutteja. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti. @@ -5497,11 +7161,27 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.This is your own one-time link! 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. + 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 **%@**. Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**. No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: Voit esittää kysymyksiä ja saada päivityksiä: @@ -5521,9 +7201,8 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.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ä. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -5531,6 +7210,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5538,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. 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 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. Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia. @@ -5548,25 +7248,53 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Voit paljastaa piilotetun profiilisi syöttämällä koko salasanan hakukenttään **Keskusteluprofiilisi** -sivulla. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi. No comment provided by engineer. + + To 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. Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia. No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. + + Token status: %@. + token status + + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation Kuljetuksen eristäminen 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: %@). Yritetään muodostaa yhteyttä palvelimeen, jota käytetään tämän kontaktin viestien vastaanottamiseen (virhe: %@). @@ -5616,10 +7344,9 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unblock member? No comment provided by engineer. - - Unexpected error: %@ - Odottamaton virhe: %@ - item status description + + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5629,7 +7356,7 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unfav. Epäsuotuisa. - No comment provided by engineer. + swipe action Unhide @@ -5666,6 +7393,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Tuntematon virhe No comment provided by engineer. + + Unknown servers! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Ellet käytä iOS:n puhelinkäyttöliittymää, ota Älä häiritse -tila käyttöön keskeytysten välttämiseksi. @@ -5699,11 +7430,15 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Unmute Poista mykistys - No comment provided by engineer. + notification label action Unread Lukematon + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5715,11 +7450,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja 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 @@ -5730,9 +7460,12 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Päivitä verkkoasetukset? No comment provided by engineer. - - Update transport isolation mode? - Päivitä kuljetuksen eristystila? + + Update settings? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5740,16 +7473,15 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja 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 No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5759,20 +7491,44 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Lataa tiedosto server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Käytä .onion-isäntiä No comment provided by engineer. + + Use SOCKS proxy + No comment provided by engineer. + Use SimpleX Chat servers? Käytä SimpleX Chat palvelimia? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Käytä chattia @@ -5783,6 +7539,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Käytä nykyistä profiilia 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 Käytä uusiin yhteyksiin @@ -5806,23 +7570,45 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + No comment provided by engineer. + Use server Käytä palvelinta No comment provided by engineer. + + Use servers + No comment provided by engineer. + + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. - - User profile - Käyttäjäprofiili + + Use the app with one hand. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - .onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan. + + Use web port + No comment provided by engineer. + + + User selection + No comment provided by engineer. + + + Username No comment provided by engineer. @@ -5889,11 +7675,19 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Videot ja tiedostot 1 Gt asti No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Näytä turvakoodi No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history chat feature @@ -5908,8 +7702,8 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Ääniviestit ovat kiellettyjä tässä keskustelussa. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Ääniviestit ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. @@ -5946,6 +7740,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Odottaa videota No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + 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. @@ -5988,9 +7790,8 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja When connecting audio and video calls. 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. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -6023,24 +7824,34 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + alert message + Wrong database passphrase Väärä tietokannan tunnuslause No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Väärä tunnuslause! No comment provided by engineer. - - XFTP servers - XFTP-palvelimet - No comment provided by engineer. - - - You - Sinä + + XFTP server No comment provided by engineer. @@ -6067,6 +7878,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Olet jo muodostanut yhteyden %@:n kanssa. 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. @@ -6106,11 +7921,23 @@ Repeat join request? Sinut on kutsuttu ryhmään 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 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 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 Voit luoda sen myöhemmin @@ -6139,11 +7966,19 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Voit nyt lähettää viestejä %@:lle notification body + + 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. Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista. @@ -6159,16 +7994,15 @@ Repeat join request? Voit jakaa tämän osoitteen kontaktiesi kanssa, jotta ne voivat muodostaa yhteyden **%@** kanssa. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Voit jakaa osoitteesi linkkinä tai QR-koodina - kuka tahansa voi muodostaa yhteyden sinuun. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Voit ottaa SimpleX Lockin käyttöön Asetusten kautta. @@ -6181,23 +8015,23 @@ Repeat join request? You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! Et voi lähettää viestejä! 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 decide who can connect. + Kimin bağlanabileceğine siz karar verirsiniz. + No comment provided by engineer. + You have already requested connection via this address! No comment provided by engineer. @@ -6207,11 +8041,6 @@ Repeat join request? Repeat connection request? No comment provided by engineer. - - You have no chats - Sinulla ei ole keskusteluja - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen. @@ -6232,11 +8061,23 @@ Repeat connection request? Liityit tähän ryhmään. Muodostetaan yhteyttä ryhmän jäsenten kutsumiseksi. 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. Sinun tulee käyttää keskustelujen-tietokannan uusinta versiota AINOSTAAN yhdessä laitteessa, muuten saatat lakata vastaanottamasta viestejä joiltakin kontakteilta. No comment provided by engineer. + + You need to allow your contact to 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. Sinun on sallittava kontaktiesi lähettää ääniviestejä, jotta voit lähettää niitä. @@ -6252,6 +8093,10 @@ Repeat connection request? Lähetit ryhmäkutsun No comment provided by engineer. + + You should receive notifications. + token info + You will be connected to group when the group host's device is online, please wait or check later! Sinut yhdistetään ryhmään, kun ryhmän isännän laite on online-tilassa, odota tai tarkista myöhemmin! @@ -6285,6 +8130,10 @@ Repeat connection request? Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Et enää saa viestejä tästä ryhmästä. Keskusteluhistoria säilytetään. @@ -6305,31 +8154,16 @@ Repeat connection request? 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 ICE servers ICE-palvelimesi No comment provided by engineer. - - Your SMP servers - SMP-palvelimesi - No comment provided by engineer. - Your SimpleX address SimpleX-osoitteesi No comment provided by engineer. - - Your XFTP servers - XFTP-palvelimesi - No comment provided by engineer. - Your calls Puhelusi @@ -6345,16 +8179,17 @@ Repeat connection request? Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Keskusteluprofiilisi No comment provided by engineer. - - 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). - Kontaktin tulee olla online-tilassa, jotta yhteys voidaan muodostaa. -Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uudella linkillä). + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. No comment provided by engineer. @@ -6372,6 +8207,10 @@ Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uu Kontaktisi pysyvät yhdistettyinä. No comment provided by engineer. + + Your credentials may be sent unencrypted. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Nykyinen keskustelut-tietokantasi poistetaan ja korvataan tuodulla tietokannalla. @@ -6401,33 +8240,34 @@ Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uu Profiilisi **%@** jaetaan. No comment provided by engineer. - - 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. + + Your profile is stored on your device and only shared with your contacts. + Profiili jaetaan vain kontaktiesi kanssa. No comment provided by engineer. - - Your profile, contacts and delivered messages are stored on your device. - Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi. + + 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 was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your random profile Satunnainen profiilisi No comment provided by engineer. - - Your server - Palvelimesi - No comment provided by engineer. - Your server address Palvelimesi osoite No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Asetuksesi @@ -6468,6 +8308,10 @@ SimpleX-palvelimet eivät näe profiiliasi. hyväksytty puhelu call status + + accepted invitation + chat list item title + admin ylläpitäjä @@ -6500,6 +8344,14 @@ SimpleX-palvelimet eivät näe profiiliasi. and %lld other events No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) äänipuhelu (ei e2e-salattu) @@ -6529,13 +8381,18 @@ SimpleX-palvelimet eivät näe profiiliasi. blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold lihavoitu No comment provided by engineer. + + call + No comment provided by engineer. + call error soittovirhe @@ -6638,7 +8495,7 @@ SimpleX-palvelimet eivät näe profiiliasi. connecting… yhdistää… - chat list item title + No comment provided by engineer. connection established @@ -6684,10 +8541,15 @@ SimpleX-palvelimet eivät näe profiiliasi. päivää time unit + + decryption errors + No comment provided by engineer. + default (%@) oletusarvo (%@) - pref value + delete after time +pref value default (no) @@ -6733,6 +8595,10 @@ SimpleX-palvelimet eivät näe profiiliasi. päällekkäinen viesti integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e-salattu @@ -6808,9 +8674,8 @@ SimpleX-palvelimet eivät näe profiiliasi. virhe No comment provided by engineer. - - event happened - tapahtuma tapahtui + + expired No comment provided by engineer. @@ -6842,6 +8707,10 @@ SimpleX-palvelimet eivät näe profiiliasi. iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen sen muuttamisen tai sovelluksen uudelleen käynnistämisen jälkeen - se mahdollistaa push-ilmoitusten vastaanottamisen. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link incognito kontaktilinkin kautta @@ -6882,6 +8751,10 @@ SimpleX-palvelimet eivät näe profiiliasi. kutsu ryhmään %@ group name + + invite + No comment provided by engineer. + invited kutsuttu @@ -6936,6 +8809,10 @@ SimpleX-palvelimet eivät näe profiiliasi. yhdistetty rcv group event chat item + + message + No comment provided by engineer. + message received viesti vastaanotettu @@ -6961,6 +8838,10 @@ SimpleX-palvelimet eivät näe profiiliasi. %@ moderoi marked deleted chat item preview text + + moderator + member role + months kuukautta @@ -6969,7 +8850,7 @@ SimpleX-palvelimet eivät näe profiiliasi. never ei koskaan - No comment provided by engineer. + delete after time new message @@ -7000,8 +8881,8 @@ SimpleX-palvelimet eivät näe profiiliasi. off pois enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7018,6 +8899,14 @@ SimpleX-palvelimet eivät näe profiiliasi. päällä group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner omistaja @@ -7032,6 +8921,14 @@ SimpleX-palvelimet eivät näe profiiliasi. vertais No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -7046,6 +8943,10 @@ SimpleX-palvelimet eivät näe profiiliasi. vahvistus saatu… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call hylätty puhelu @@ -7074,6 +8975,10 @@ SimpleX-palvelimet eivät näe profiiliasi. poisti sinut rcv group event chat item + + requested to connect + chat list item title + saved No comment provided by engineer. @@ -7082,6 +8987,10 @@ SimpleX-palvelimet eivät näe profiiliasi. saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec sek @@ -7106,6 +9015,12 @@ SimpleX-palvelimet eivät näe profiiliasi. send direct message No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address profile update event chat item @@ -7142,10 +9057,18 @@ SimpleX-palvelimet eivät näe profiiliasi. tuntematon connection info + + unknown servers + No comment provided by engineer. + unknown status No comment provided by engineer. + + unprotected + No comment provided by engineer. + updated group profile päivitetty ryhmäprofiili @@ -7184,6 +9107,10 @@ SimpleX-palvelimet eivät näe profiiliasi. releellä No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) videopuhelu (ei e2e-salattu) @@ -7209,6 +9136,10 @@ SimpleX-palvelimet eivät näe profiiliasi. viikkoa time unit + + when IP hidden + No comment provided by engineer. + yes kyllä @@ -7290,7 +9221,7 @@ SimpleX-palvelimet eivät näe profiiliasi.
- +
@@ -7326,7 +9257,7 @@ SimpleX-palvelimet eivät näe profiiliasi.
- +
@@ -7346,4 +9277,205 @@ SimpleX-palvelimet eivät näe profiiliasi.
+ +
+ +
+ + + %d new events + notification body + + + From %d chat(s) + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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 + 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. + No comment provided by engineer. + + +
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 ac7fc8cc8b..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 @@
- +
- - - - - - 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é) @@ -127,6 +100,11 @@ %@ est vérifié·e No comment provided by engineer. + + %@ server + Serveur %@ + No comment provided by engineer. + %@ servers Serveurs %@ @@ -142,6 +120,11 @@ %@ veut se connecter ! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ et %lld membres @@ -162,11 +145,36 @@ %d jours time interval + + %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). + forward confirmation reason + %d hours %d heures time interval + + %d messages not forwarded + %d messages non transférés + alert title + %d min %d min @@ -182,6 +190,11 @@ %d sec time interval + + %d seconds(s) + %d seconde(s) + delete after time + %d skipped message(s) %d message·s sauté·s @@ -252,11 +265,6 @@ %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 @@ -307,11 +315,6 @@ %u messages sautés. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nouveau) @@ -322,19 +325,9 @@ (cet appareil v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Ajouter un contact** : pour créer un nouveau lien d'invitation ou vous connecter via un lien que vous avez reçu. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link 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. @@ -342,13 +335,13 @@ **Créer un groupe** : pour créer un nouveau groupe. 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. + + **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). + + **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. @@ -362,11 +355,16 @@ **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. + + **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 / 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. @@ -392,11 +390,6 @@ \*gras* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +426,6 @@ - l'historique de modification. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -451,7 +439,8 @@ 1 day 1 jour - time interval + delete after time +time interval 1 hour @@ -466,12 +455,29 @@ 1 month 1 mois - time interval + delete after time +time interval 1 week 1 semaine - time interval + delete after time +time interval + + + 1 year + 1 an + delete after time + + + 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 @@ -488,11 +494,6 @@ 30 secondes No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -529,7 +530,7 @@ Abort - Annuler + Abandonner No comment provided by engineer. @@ -542,31 +543,32 @@ Abandonner le changement d'adresse ? No comment provided by engineer. - - About SimpleX - À propos de SimpleX - No comment provided by engineer. - About SimpleX Chat À propos de SimpleX Chat No comment provided by engineer. - - About SimpleX address - À propos de l'adresse SimpleX + + About operators + À propos des opérateurs No comment provided by engineer. - - Accent color - Couleur principale + + Accent + Principale No comment provided by engineer. Accept Accepter accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Accepter les conditions + No comment provided by engineer. Accept connection request? @@ -581,21 +583,47 @@ Accept incognito Accepter en incognito - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Conditions acceptées + 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 + No comment provided by engineer. + + + Active + Actif + token status text + + + 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 contact - Ajouter le contact + + Add friends + Ajouter des amis No comment provided by engineer. - - Add preset servers - Ajouter des serveurs prédéfinis + + Add list + Ajouter une liste No comment provided by engineer. @@ -603,14 +631,19 @@ Ajouter un profil No comment provided by engineer. + + 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 server… - Ajouter un serveur… + + Add team members + Ajouter des membres à l'équipe No comment provided by engineer. @@ -618,11 +651,46 @@ 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 @@ -633,6 +701,16 @@ 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 + No comment provided by engineer. + Admins can block a member for all. Les admins peuvent bloquer un membre pour tous. @@ -648,6 +726,16 @@ Paramètres réseau avancés No comment provided by engineer. + + Advanced settings + Paramètres avancés + 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. @@ -658,16 +746,31 @@ Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière ! No comment provided by engineer. + + 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. + alert message + 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. 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 ! @@ -683,6 +786,20 @@ Tous les nouveaux messages de %@ seront cachés ! No comment provided by engineer. + + All profiles + Tous les profiles + profile dropdown + + + All reports will be archived for you. + Tous les rapports seront archivés pour vous. + No comment provided by engineer. + + + All servers + No comment provided by engineer. + All your contacts will remain connected. Tous vos contacts resteront connectés. @@ -708,11 +825,21 @@ 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 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) @@ -738,11 +865,21 @@ Autorise l’envoi de messages éphémères. No comment provided by engineer. + + 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 SimpleX links. Autorise l'envoi de liens SimpleX. @@ -808,6 +945,11 @@ Groupe déjà rejoint ! 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 @@ -818,11 +960,21 @@ Un profil de chat vierge portant le nom fourni est créé et l'application s'ouvre normalement. No comment provided by engineer. + + Another reason + Autre raison + report reason + 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 : %@ @@ -838,6 +990,10 @@ L'application chiffre les nouveaux fichiers locaux (sauf les vidéos). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon Icône de l'app @@ -853,6 +1009,11 @@ 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 @@ -873,9 +1034,54 @@ 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 transférer + 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 ? + No comment provided by engineer. + + + Archive reports + Archiver les rapports + swipe action + + + Archived contacts + Contacts archivés No comment provided by engineer. @@ -943,11 +1149,21 @@ Images auto-acceptées No comment provided by engineer. + + Auto-accept settings + Paramètres de réception automatique + alert title + Back Retour No comment provided by engineer. + + Background + Fond + No comment provided by engineer. + Bad desktop address Mauvaise adresse de bureau @@ -963,16 +1179,61 @@ Mauvais hash 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 @@ -1008,6 +1269,16 @@ 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. + Both you and your contact can add message reactions. Vous et votre contact pouvez ajouter des réactions aux messages. @@ -1038,11 +1309,35 @@ 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: +- send only legal content in public groups. +- respect other users – no spam. + En utilisant SimpleX Chat, vous acceptez de : +- n'envoyer que du contenu légal dans les groupes publics. +- respecter les autres utilisateurs - pas de spam. + No comment provided by engineer. + Call already ended! Appel déjà terminé ! @@ -1053,11 +1348,26 @@ Appels No comment provided by engineer. + + 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 ! @@ -1068,10 +1378,16 @@ Impossible d'inviter les contacts ! No comment provided by engineer. + + Can't message member + Impossible d'envoyer un message à ce membre + No comment provided by engineer. + Cancel Annuler - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1399,20 @@ 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. + Cannot receive file Impossible de recevoir le fichier - No comment provided by engineer. + alert title + + + 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. + snd error text Cellular @@ -1098,6 +1424,16 @@ Changer No comment provided by engineer. + + Change automatic message deletion? + Modifier la suppression automatique des messages ? + alert title + + + Change chat profiles + Changer de profil de discussion + authentication reason + Change database passphrase? Changer la phrase secrète de la base de données ? @@ -1142,11 +1478,26 @@ Change self-destruct passcode Modifier le code d'autodestruction authentication reason - set passcode view +set passcode view - - 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. @@ -1164,6 +1515,11 @@ 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 @@ -1184,6 +1540,11 @@ 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 ! @@ -1194,15 +1555,50 @@ Préférences de chat No comment provided by engineer. + + Chat preferences were changed. + Les préférences de discussion ont été modifiées. + alert message + + + 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. + No comment provided by engineer. + Check server address and try again. Vérifiez l'adresse du serveur et réessayez. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1620,25 @@ 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 + No comment provided by engineer. + Clear Effacer - No comment provided by engineer. + swipe action Clear conversation @@ -1239,6 +1650,16 @@ 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 ? @@ -1249,11 +1670,21 @@ Retirer la vérification No comment provided by engineer. - - Colors - Couleurs + + 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. + + + Community guidelines violation + Infraction aux règles communautaires + report reason + Compare file Comparer le fichier @@ -1264,11 +1695,56 @@ Comparez les codes de sécurité avec vos contacts. 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 @@ -1279,11 +1755,21 @@ Confirmer le code d'accès 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 @@ -1309,6 +1795,11 @@ Confirmer la transmission No comment provided by engineer. + + Confirmed + Confirmé + token status text + Connect Se connecter @@ -1329,6 +1820,11 @@ Connexion au bureau 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 ? @@ -1368,16 +1864,31 @@ Il s'agit de votre propre lien unique ! Se connecter avec %@ No comment provided by engineer. + + Connected + Connecté + No comment provided by engineer. + Connected desktop Bureau connecté 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 to server… Connexion au serveur… @@ -1388,6 +1899,11 @@ Il s'agit de votre propre lien unique ! 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 @@ -1398,6 +1914,16 @@ Il s'agit de votre propre lien unique ! 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 @@ -1408,11 +1934,38 @@ Il s'agit de votre propre lien unique ! Erreur de connexion (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + La connexion est bloquée par l'opérateur du serveur : +%@ + 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 @@ -1423,6 +1976,16 @@ Il s'agit de votre propre lien unique ! Délai de connexion No comment provided by engineer. + + Connection with desktop stopped + La connexion avec le bureau s'est arrêtée + No comment provided by engineer. + + + Connections + Connexions + No comment provided by engineer. + Contact allows Votre contact autorise @@ -1433,6 +1996,11 @@ Il s'agit de votre propre lien unique ! Contact déjà existant No comment provided by engineer. + + Contact deleted! + Contact supprimé ! + No comment provided by engineer. + Contact hidden: Contact masqué : @@ -1443,9 +2011,9 @@ Il s'agit de votre propre lien unique ! Le contact est connecté notification - - Contact is not connected yet! - Le contact n'est pas encore connecté ! + + Contact is deleted. + Le contact est supprimé. No comment provided by engineer. @@ -1458,6 +2026,11 @@ Il s'agit de votre propre lien unique ! 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 @@ -1468,21 +2041,41 @@ Il s'agit de votre propre lien unique ! Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter. No comment provided by engineer. + + Content violates conditions of use + Le contenu enfreint les conditions d'utilisation + blocking reason + Continue Continuer No comment provided by engineer. + + Conversation deleted! + Conversation supprimée ! + No comment provided by engineer. + Copy Copier - chat item action + 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 %@ ? @@ -1493,6 +2086,11 @@ Il s'agit de votre propre lien unique ! Créer No comment provided by engineer. + + Create 1-time link + Créer un lien unique + No comment provided by engineer. + Create SimpleX address Créer une adresse SimpleX @@ -1503,11 +2101,6 @@ Il s'agit de votre propre lien unique ! Création de groupes via un profil aléatoire. No comment provided by engineer. - - Create an address to let people connect with you. - Vous pouvez créer une adresse pour permettre aux autres utilisateurs de vous contacter. - No comment provided by engineer. - Create file Créer un fichier @@ -1528,6 +2121,11 @@ Il s'agit de votre propre lien unique ! Créer un lien No comment provided by engineer. + + Create list + Créer une liste + 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/). 💻 @@ -1553,6 +2151,11 @@ Il s'agit de votre propre lien unique ! Créez votre profil No comment provided by engineer. + + Created + Créées + No comment provided by engineer. + Created at Créé à @@ -1563,11 +2166,6 @@ Il s'agit de votre propre lien unique ! Créé à : %@ copied message info - - Created on %@ - Créé le %@ - No comment provided by engineer. - Creating archive link Création d'un lien d'archive @@ -1583,11 +2181,21 @@ Il s'agit de votre propre lien unique ! Code d'accès actuel 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 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 %@. @@ -1598,11 +2206,26 @@ Il s'agit de votre propre lien unique ! 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 ID ID de base de données @@ -1701,6 +2324,11 @@ Il s'agit de votre propre lien unique ! La base de données sera migrée lors du redémarrage de l'app No comment provided by engineer. + + Debug delivery + Livraison de débogage + No comment provided by engineer. + Decentralized Décentralisé @@ -1714,18 +2342,19 @@ Il s'agit de votre propre lien unique ! Delete Supprimer - chat item action + alert action +swipe action + + + Delete %lld messages of members? + Supprimer %lld messages de membres ? + No comment provided by engineer. Delete %lld messages? Supprimer %lld messages ? No comment provided by engineer. - - Delete Contact - Supprimer le contact - No comment provided by engineer. - Delete address Supprimer l'adresse @@ -1751,14 +2380,14 @@ Il s'agit de votre propre lien unique ! 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. @@ -1771,6 +2400,11 @@ Il s'agit de votre propre lien unique ! 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 @@ -1781,11 +2415,9 @@ Il s'agit de votre propre lien unique ! Supprimer le contact No comment provided by engineer. - - Delete contact? -This cannot be undone! - Supprimer le contact ? -Cette opération ne peut être annulée ! + + Delete contact? + Supprimer le contact ? No comment provided by engineer. @@ -1848,6 +2480,11 @@ Cette opération ne peut être annulée ! Supprimer le lien ? No comment provided by engineer. + + Delete list? + Supprimer la liste ? + alert title + Delete member message? Supprimer le message de ce membre ? @@ -1861,7 +2498,7 @@ Cette opération ne peut être annulée ! Delete messages Supprimer les messages - No comment provided by engineer. + alert button Delete messages after @@ -1878,9 +2515,9 @@ Cette opération ne peut être annulée ! 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. @@ -1898,11 +2535,31 @@ Cette opération ne peut être annulée ! Supprimer la file d'attente server test step + + 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 + No comment provided by engineer. + + + Deleted + Supprimées + No comment provided by engineer. + Deleted at Supprimé à @@ -1913,6 +2570,16 @@ Cette opération ne peut être annulée ! Supprimé à : %@ copied message info + + 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 @@ -1925,7 +2592,7 @@ Cette opération ne peut être annulée ! Delivery receipts! - Justificatifs de réception! + Justificatifs de réception ! No comment provided by engineer. @@ -1948,11 +2615,41 @@ Cette opération ne peut être annulée ! 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 %@. + No comment provided by engineer. + + + Destination server error: %@ + Erreur du serveur de destination : %@ + snd error text + + + 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 @@ -1983,8 +2680,13 @@ Cette opération ne peut être annulée ! Messages directs chat feature - - Direct messages between members are prohibited in this group. + + 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. @@ -1998,11 +2700,26 @@ Cette opération ne peut être annulée ! Désactiver SimpleX Lock authentication reason + + Disable automatic message deletion? + Désactiver la suppression automatique des messages ? + alert title + + + Disable delete messages + Désactiver la suppression des messages + alert button + Disable for all Désactiver pour tous No comment provided by engineer. + + Disabled + Désactivé + No comment provided by engineer. + Disappearing message Message éphémère @@ -2018,8 +2735,8 @@ Cette opération ne peut être annulée ! Les messages éphémères sont interdits dans cette discussion. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Les messages éphémères sont interdits dans ce groupe. No comment provided by engineer. @@ -2053,11 +2770,21 @@ Cette opération ne peut être annulée ! Rechercher sur le réseau 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 SimpleX for emergency calls. N'utilisez PAS SimpleX pour les appels d'urgence. No comment provided by engineer. + + Do NOT use private routing. + Ne pas utiliser de routage privé. + No comment provided by engineer. + Do it later Faites-le plus tard @@ -2068,6 +2795,16 @@ Cette opération ne peut être annulée ! Ne pas envoyer d'historique aux nouveaux membres. No comment provided by engineer. + + Do not use credentials with proxy. + Ne pas utiliser d'identifiants avec le proxy. + No comment provided by engineer. + + + Documents: + Documents: + No comment provided by engineer. + Don't create address Ne pas créer d'adresse @@ -2078,11 +2815,21 @@ Cette opération ne peut être annulée ! 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 @@ -2091,7 +2838,13 @@ Cette opération ne peut être annulée ! Download Télécharger - chat item action + alert button +chat item action + + + Download errors + Erreurs de téléchargement + No comment provided by engineer. Download failed @@ -2103,6 +2856,21 @@ Cette opération ne peut être annulée ! Télécharger le fichier server test step + + Download files + Télécharger les fichiers + alert action + + + 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 @@ -2123,6 +2891,11 @@ Cette opération ne peut être annulée ! Durée No comment provided by engineer. + + E2E encrypted notifications. + Notifications chiffrées E2E. + No comment provided by engineer. + Edit Modifier @@ -2143,6 +2916,11 @@ Cette opération ne peut être annulée ! Activer (conserver les remplacements) 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 SimpleX Lock Activer SimpleX Lock @@ -2156,7 +2934,7 @@ Cette opération ne peut être annulée ! Enable automatic message deletion? Activer la suppression automatique des messages ? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2981,11 @@ Cette opération ne peut être annulée ! Activer le code d'autodestruction set passcode view + + Enabled + Activé + No comment provided by engineer. + Enabled for Activé pour @@ -2278,6 +3061,11 @@ Cette opération ne peut être annulée ! La renégociation du chiffrement a échoué. No comment provided by engineer. + + Encryption renegotiation in progress. + Renégociation du chiffrement en cours. + No comment provided by engineer. + Enter Passcode Entrer le code d'accès @@ -2343,26 +3131,36 @@ Cette opération ne peut être annulée ! Erreur lors de l'annulation du changement d'adresse No comment provided by engineer. + + Error accepting conditions + Erreur lors de la validation des conditions + alert title + 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 No comment provided by engineer. + + Error adding server + Erreur lors de l'ajout du serveur + alert title + 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 @@ -2373,6 +3171,21 @@ Cette opération ne peut être annulée ! 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 @@ -2388,6 +3201,11 @@ Cette opération ne peut être annulée ! Erreur lors de la création du lien du groupe No comment provided by engineer. + + Error creating list + Erreur lors de la création de la liste + alert title + Error creating member contact Erreur lors de la création du contact du membre @@ -2403,6 +3221,11 @@ Cette opération ne peut être annulée ! 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 @@ -2423,11 +3246,6 @@ Cette opération ne peut être annulée ! 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 @@ -2473,6 +3291,11 @@ Cette opération ne peut être annulée ! 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 @@ -2483,9 +3306,14 @@ Cette opération ne peut être annulée ! Erreur lors de la liaison avec le groupe No comment provided by engineer. - - Error loading %@ servers - Erreur lors du chargement des serveurs %@ + + Error loading servers + Erreur de chargement des serveurs + alert title + + + Error migrating settings + Erreur lors de la migration des paramètres No comment provided by engineer. @@ -2496,16 +3324,36 @@ Cette opération ne peut être annulée ! Error receiving file Erreur lors de la réception du fichier + alert title + + + Error reconnecting server + Erreur de reconnexion du serveur No comment provided by engineer. + + Error reconnecting servers + Erreur de reconnexion des serveurs + No comment provided by engineer. + + + Error registering for notifications + Erreur lors de l'inscription aux notifications + alert title + Error removing member Erreur lors de la suppression d'un membre No comment provided by engineer. - - Error saving %@ servers - Erreur lors de la sauvegarde des serveurs %@ + + Error reordering lists + Erreur lors de la réorganisation des listes + alert title + + + Error resetting statistics + Erreur de réinitialisation des statistiques No comment provided by engineer. @@ -2513,6 +3361,11 @@ Cette opération ne peut être annulée ! Erreur lors de la sauvegarde des serveurs ICE No comment provided by engineer. + + Error saving chat list + Erreur lors de l'enregistrement de la liste des chats + alert title + Error saving group profile Erreur lors de la sauvegarde du profil de groupe @@ -2528,6 +3381,11 @@ Cette opération ne peut être annulée ! Erreur lors de l'enregistrement de la phrase de passe dans la keychain No comment provided by engineer. + + Error saving servers + Erreur d'enregistrement des serveurs + alert title + Error saving settings Erreur lors de l'enregistrement des paramètres @@ -2573,16 +3431,26 @@ Cette opération ne peut être annulée ! Erreur lors de l'arrêt du chat No comment provided by engineer. + + Error switching profile + Erreur lors du changement de profil + No comment provided by engineer. + Error switching profile! Erreur lors du changement de profil ! - No comment provided by engineer. + alertTitle 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 @@ -2593,6 +3461,11 @@ Cette opération ne peut être annulée ! Erreur lors de la mise à jour du message No comment provided by engineer. + + Error updating server + Erreur de mise à jour du serveur + alert title + Error updating settings Erreur lors de la mise à jour des paramètres @@ -2621,7 +3494,9 @@ Cette opération ne peut être annulée ! Error: %@ Erreur : %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3508,16 @@ Cette opération ne peut être annulée ! Erreur : pas de fichier de base de données No comment provided by engineer. + + Errors + Erreurs + No comment provided by engineer. + + + Errors in servers configuration. + Erreurs dans la configuration des serveurs. + servers error + Even when disabled in the conversation. Même s'il est désactivé dans la conversation. @@ -2648,6 +3533,11 @@ Cette opération ne peut être annulée ! Étendre chat item action + + Expired + Expiré + token status text + Export database Exporter la base de données @@ -2658,6 +3548,11 @@ Cette opération ne peut être annulée ! 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. @@ -2683,16 +3578,70 @@ Cette opération ne peut être annulée ! 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. + No comment provided by engineer. + Favorite Favoris + swipe action + + + Favorites + Favoris No comment provided by engineer. + + File error + Erreur de fichier + file error alert title + + + File errors: +%@ + Erreurs de fichier : +%@ + alert message + + + File is blocked by server operator: +%@. + Le fichier est bloqué par l'opérateur du serveur : +%@. + 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 : %@ + file error text + + + File status + Statut du fichier + No comment provided by engineer. + + + File status: %@ + Statut du fichier : %@ + copied message info + File will be deleted from servers. Le fichier sera supprimé des serveurs. @@ -2713,6 +3662,11 @@ Cette opération ne peut être annulée ! Fichier : %@ No comment provided by engineer. + + Files + Fichiers + No comment provided by engineer. + Files & media Fichiers & médias @@ -2723,8 +3677,8 @@ Cette opération ne peut être annulée ! Fichiers et médias chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Les fichiers et les médias sont interdits dans ce groupe. No comment provided by engineer. @@ -2775,7 +3729,7 @@ Cette opération ne peut être annulée ! Fix connection? - Réparer la connexion? + Réparer la connexion ? No comment provided by engineer. @@ -2793,21 +3747,69 @@ Cette opération ne peut être annulée ! Correction non prise en charge par un membre du groupe No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + Pour le profil de discussion %@ : + servers error + 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 me + 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 + No comment provided by engineer. + Forward Transférer chat item action + + Forward %d message(s)? + Transférer %d message(s) ? + alert title + Forward and save messages Transférer et sauvegarder des messages No comment provided by engineer. + + Forward messages + Transférer les messages + alert action + + + Forward messages without files? + Transférer les messages sans les fichiers ? + alert message + + + Forward up to 20 messages at once. + Transférez jusqu'à 20 messages à la fois. + No comment provided by engineer. + Forwarded Transféré @@ -2818,6 +3820,40 @@ Cette opération ne peut être annulée ! 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 : %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Serveur de transfert : %1$@ +Erreur du serveur de destination : %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Serveur de transfert : %1$@ +Erreur : %2$@ + snd error text + Found desktop Bureau trouvé @@ -2838,11 +3874,6 @@ Cette opération ne peut être annulée ! 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. @@ -2863,6 +3894,20 @@ Cette opération ne peut être annulée ! GIFs et stickers No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + Bonjour ! + message preview + + + Good morning! + Bonjour ! + message preview + Group Groupe @@ -2918,41 +3963,6 @@ Cette opération ne peut être annulée ! 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. (24 hours) - Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures) - No comment provided by engineer. - - - Group members can send SimpleX links. - Les membres du groupe peuvent envoyer des liens SimpleX. - 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. - No comment provided by engineer. - Group message: Message du groupe : @@ -2993,11 +4003,19 @@ Cette opération ne peut être annulée ! Le groupe va être supprimé pour vous - impossible de revenir en arrière ! No comment provided by engineer. + + Groups + No comment provided by engineer. + Help Aide No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Caché @@ -3048,10 +4066,19 @@ Cette opération ne peut être annulée ! Comment SimpleX fonctionne No comment provided by engineer. + + 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 it works - Comment ça fonctionne - No comment provided by engineer. + alert button How to @@ -3078,6 +4105,11 @@ Cette opération ne peut être annulée ! Serveurs ICE (un par ligne) No comment provided by engineer. + + IP address + Adresse IP + 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. @@ -3118,8 +4150,8 @@ Cette opération ne peut être annulée ! Immédiatement No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Protégé du spam et des abus No comment provided by engineer. @@ -3143,11 +4175,23 @@ Cette opération ne peut être annulée ! É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. +More improvements are coming soon! + Amélioration de la distribution, réduction de l'utilisation du trafic. +D'autres améliorations sont à venir ! + No comment provided by engineer. + Improved message delivery Amélioration de la transmission des messages @@ -3178,6 +4222,14 @@ Cette opération ne peut être annulée ! Sons d'appel No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Incognito @@ -3248,6 +4300,11 @@ Cette opération ne peut être annulée ! 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! @@ -3255,16 +4312,36 @@ Cette opération ne peut être annulée ! No comment provided by engineer. - - Instantly - Instantané - No comment provided by engineer. - Interface Interface No comment provided by engineer. + + Interface colors + Couleurs d'interface + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code Code QR invalide @@ -3303,7 +4380,7 @@ Cette opération ne peut être annulée ! Invalid server address! Adresse de serveur invalide ! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4402,11 @@ Cette opération ne peut être annulée ! Inviter des membres No comment provided by engineer. + + Invite to chat + Inviter à discuter + No comment provided by engineer. + Invite to group Inviter au groupe @@ -3340,8 +4422,8 @@ Cette opération ne peut être annulée ! La suppression irréversible de message est interdite dans ce chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. La suppression irréversible de messages est interdite dans ce groupe. No comment provided by engineer. @@ -3366,6 +4448,11 @@ Cette opération ne peut être annulée ! 3. 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 (%@). @@ -3384,7 +4471,7 @@ Cette opération ne peut être annulée ! Join Rejoindre - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4513,11 @@ Voici votre lien pour le groupe %@ ! Keep Conserver + alert action + + + Keep conversation + Garder la conversation No comment provided by engineer. @@ -3436,7 +4528,7 @@ Voici votre lien pour le groupe %@ ! Keep unused invitation? Conserver l'invitation inutilisée ? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4563,16 @@ Voici votre lien pour le groupe %@ ! Leave Quitter + swipe action + + + Leave chat + Quitter la discussion + No comment provided by engineer. + + + Leave chat? + Quitter la discussion ? No comment provided by engineer. @@ -3513,6 +4615,18 @@ Voici votre lien pour le groupe %@ ! Bureaux liés No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! Message dynamique ! @@ -3523,11 +4637,6 @@ Voici votre lien pour le groupe %@ ! Messages dynamiques No comment provided by engineer. - - Local - Local - No comment provided by engineer. - Local name Nom local @@ -3548,11 +4657,6 @@ Voici votre lien pour le groupe %@ ! 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 @@ -3563,21 +4667,11 @@ Voici votre lien pour le groupe %@ ! 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 @@ -3603,11 +4697,35 @@ Voici votre lien pour le groupe %@ ! Max 30 secondes, réception immédiate. No comment provided by engineer. + + Media & file servers + Serveurs de fichiers et de médias + No comment provided by engineer. + + + Medium + Modéré + blur media + Member Membre No comment provided by engineer. + + Member inactive + Membre inactif + item status text + + + Member reports + chat feature + + + 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. @@ -3618,11 +4736,64 @@ Voici votre lien pour le groupe %@ ! 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 report messsages to moderators. + 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 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 voice messages. + Les membres du groupe peuvent envoyer des messages vocaux. + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + Menus + No comment provided by engineer. + Message delivery error Erreur de distribution du message @@ -3633,11 +4804,31 @@ Voici votre lien pour le groupe %@ ! Accusés de réception des messages ! No comment provided by engineer. + + Message delivery warning + Avertissement sur la distribution des messages + item status text + Message draft Brouillon de message No comment provided by engineer. + + Message forwarded + Message transféré + item status text + + + Message may be delivered later if member becomes active. + Le message peut être transmis plus tard si le membre devient actif. + item status description + + + Message queue info + Informations sur la file d'attente des messages + No comment provided by engineer. + Message reactions Réactions aux messages @@ -3648,16 +4839,41 @@ Voici votre lien pour le groupe %@ ! Les réactions aux messages sont interdites dans ce chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Les réactions aux messages sont interdites dans ce groupe. 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 + No comment provided by engineer. + + + Message status: %@ + Statut du message : %@ + copied message info + Message text Texte du message @@ -3683,6 +4899,25 @@ Voici votre lien pour le groupe %@ ! Les messages de %@ seront affichés ! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + Messages reçus + No comment provided by engineer. + + + Messages sent + Messages envoyés + No comment provided by engineer. + + + Messages were deleted after you selected them. + Les messages ont été supprimés après avoir été sélectionnés. + alert message + 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. @@ -3748,9 +4983,9 @@ Voici votre lien pour le groupe %@ ! La migration est terminée No comment provided by engineer. - - Migrations: %@ - Migrations : %@ + + Migrations: + Migrations : No comment provided by engineer. @@ -3768,6 +5003,10 @@ Voici votre lien pour le groupe %@ ! Modéré à : %@ copied message info + + More + swipe action + More improvements are coming soon! Plus d'améliorations à venir ! @@ -3778,16 +5017,16 @@ Voici votre lien pour le groupe %@ ! Connexion réseau plus fiable. No comment provided by engineer. + + More reliable notifications + Notifications plus fiables + No comment provided by engineer. + Most likely this connection is deleted. Connexion probablement supprimée. item status description - - 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 @@ -3796,7 +5035,11 @@ Voici votre lien pour le groupe %@ ! Mute Muet - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3806,7 +5049,7 @@ Voici votre lien pour le groupe %@ ! Name Nom - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +5061,26 @@ Voici votre lien pour le groupe %@ ! Connexion au réseau No comment provided by engineer. + + Network decentralization + Décentralisation du réseau + No comment provided by engineer. + + + 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. + snd error text + 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 @@ -3833,16 +5091,35 @@ Voici votre lien pour le groupe %@ ! État du réseau No comment provided by engineer. + + New + token status text + New Passcode Nouveau code d'accès 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. + No comment provided by engineer. + New chat Nouveau chat No comment provided by engineer. + + New chat experience 🎉 + Nouvelle expérience de discussion 🎉 + No comment provided by engineer. + New contact request Nouvelle demande de contact @@ -3853,11 +5130,6 @@ Voici votre lien pour le groupe %@ ! Nouveau contact : notification - - New database archive - Nouvelle archive de base de données - No comment provided by engineer. - New desktop app! Nouvelle application de bureau ! @@ -3868,11 +5140,21 @@ Voici votre lien pour le groupe %@ ! Nouveau nom d'affichage No comment provided by engineer. + + New events + Nouveaux événements + notification + 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 @@ -3888,6 +5170,11 @@ Voici votre lien pour le groupe %@ ! Nouvelle phrase secrète… No comment provided by engineer. + + New server + Nouveau serveur + No comment provided by engineer. + No Non @@ -3898,6 +5185,18 @@ Voici votre lien pour le groupe %@ ! Pas de mot de passe pour l'app Authentication unavailable + + 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 Aucun contact sélectionné @@ -3918,6 +5217,11 @@ Voici votre lien pour le groupe %@ ! Pas de token d'appareil ! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Pas de connexion directe pour l'instant, le message est transmis par l'administrateur. + item status description + No filtered chats Aucune discussion filtrés @@ -3933,26 +5237,107 @@ Voici votre lien pour le groupe %@ ! Aucun historique No comment provided by engineer. + + No info, try to reload + Pas d'info, essayez de recharger + No comment provided by engineer. + + + No media & file servers. + Pas de serveurs de médias et de fichiers. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + Pas de serveurs de messages. + servers error + 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é No comment provided by engineer. + + 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. + servers error + + + No token! + alert title + + + No unread chats + 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. + + Notes + No comment provided by engineer. + + + Nothing selected + Aucune sélection + No comment provided by engineer. + + + Nothing to forward! + Rien à transférer ! + alert title + Notifications Notifications @@ -3963,6 +5348,19 @@ Voici votre lien pour le groupe %@ ! Les notifications sont désactivées ! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + Notifications sécurisées + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5378,35 @@ Voici votre lien pour le groupe %@ ! Off Off - No comment provided by engineer. + blur media Ok Ok - No comment provided by engineer. + alert button Old database Ancienne base de données No comment provided by engineer. - - Old database archive - Archives de l'ancienne base de données - No comment provided by engineer. - 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. +Requires compatible VPN. + Les hôtes .onion seront **nécessaires** pour la connexion. +Né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. +Requires compatible VPN. + Les hôtes .onion seront utilisés dès que possible. +Nécessite l'activation d'un VPN. No comment provided by engineer. @@ -4017,11 +5414,21 @@ Voici votre lien pour le groupe %@ ! 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**. + + 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. @@ -4037,6 +5444,14 @@ Voici votre lien pour le groupe %@ ! Seuls les propriétaires de groupes peuvent activer les messages vocaux. 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. Vous seul pouvez ajouter des réactions aux messages. @@ -4090,13 +5505,18 @@ Voici votre lien pour le groupe %@ ! Open Ouvrir - No comment provided by engineer. + alert action Open Settings Ouvrir les Paramètres No comment provided by engineer. + + Open changes + Ouvrir les modifications + No comment provided by engineer. + Open chat Ouvrir le chat @@ -4107,31 +5527,45 @@ Voici votre lien pour le groupe %@ ! Ouvrir la console du chat authentication reason + + Open conditions + Ouvrir les conditions + No comment provided by engineer. + Open group Ouvrir le groupe No comment provided by engineer. + + Open link? + alert title + Open migration to another device Ouvrir le transfert vers un autre appareil authentication reason - - Open user profiles - Ouvrir les profils d'utilisateurs - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Protocole et code open-source – n'importe qui peut heberger un serveur. - No comment provided by engineer. - Opening app… Ouverture de l'app… No comment provided by engineer. + + Operator + Opérateur + No comment provided by engineer. + + + Operator server + Serveur de l'opérateur + alert title + + + 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 @@ -4149,7 +5583,16 @@ Voici votre lien pour le groupe %@ ! Or show this code - Ou présenter ce code + Ou montrez ce code + No comment provided by engineer. + + + Or to share privately + Ou à partager en privé + No comment provided by engineer. + + + Organize chats into lists No comment provided by engineer. @@ -4157,6 +5600,13 @@ Voici votre lien pour le groupe %@ ! Autres No comment provided by engineer. + + Other file errors: +%@ + Autres erreurs de fichiers : +%@ + alert message + PING count Nombre de PING @@ -4192,6 +5642,11 @@ Voici votre lien pour le groupe %@ ! Code d'accès défini ! No comment provided by engineer. + + Password + Mot de passe + No comment provided by engineer. + Password to show Mot de passe à entrer @@ -4222,13 +5677,13 @@ Voici votre lien pour le groupe %@ ! Collez le lien que vous avez reçu 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 + + Periodic Périodique No comment provided by engineer. @@ -4242,11 +5697,28 @@ Voici votre lien pour le groupe %@ ! Appels picture-in-picture 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. +Please 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. +Veuillez 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. @@ -4314,11 +5786,28 @@ Erreur : %@ Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS la changer si vous la perdez. No comment provided by engineer. + + 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. + token info + Polish interface Interface en polonais No comment provided by engineer. + + Port + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte @@ -4329,41 +5818,83 @@ Erreur : %@ Conserver le brouillon du dernier message, avec les pièces jointes. No comment provided by engineer. - - Preset server - Serveur prédéfini - No comment provided by engineer. - Preset server address Adresse du serveur prédéfinie No comment provided by engineer. + + 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 policy and conditions of use. + No comment provided by engineer. + Privacy redefined La vie privée redéfinie No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + No comment provided by engineer. + Private filenames Noms de fichiers privés No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + + + Private message routing + Routage privé des messages + No comment provided by engineer. + + + Private message routing 🚀 + Routage privé des messages 🚀 + No comment provided by engineer. + Private notes Notes privées name of notes to self + + 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 @@ -4379,25 +5910,20 @@ Erreur : %@ Images de profil No comment provided by engineer. - - Profile name - Nom du profil - No comment provided by engineer. - - - Profile name: - Nom du profil : - No comment provided by engineer. - Profile password Mot de passe de profil No comment provided by engineer. + + Profile theme + Thème de profil + No comment provided by engineer. + Profile update will be sent to your contacts. La mise à jour du profil sera envoyée à vos contacts. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,6 +5945,10 @@ Erreur : %@ Interdire les réactions aux messages. No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. Interdire l'envoi de liens SimpleX. @@ -4444,11 +5974,23 @@ Erreur : %@ Interdire l'envoi de messages vocaux. No comment provided by engineer. + + Protect IP address + Protéger l'adresse IP + No comment provided by engineer. + Protect app screen Protéger l'écran de l'app No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Protégez votre adresse IP des relais de messagerie choisis par vos contacts. +Activez-le dans les paramètres *Réseau et serveurs*. + No comment provided by engineer. + Protect your chat profiles with a password! Protégez vos profils de chat par un mot de passe ! @@ -4464,6 +6006,21 @@ Erreur : %@ 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 @@ -4484,6 +6041,11 @@ Erreur : %@ Évaluer l'app No comment provided by engineer. + + Reachable chat toolbar + Barre d'outils accessible + No comment provided by engineer. + React… Réagissez… @@ -4492,33 +6054,28 @@ Erreur : %@ Read Lire - No comment provided by engineer. + swipe action 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). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Plus d'informations sur notre GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Pour en savoir plus, consultez notre [dépôt GitHub](https://github.com/simplex-chat/simplex-chat#readme). @@ -4529,6 +6086,11 @@ Erreur : %@ Les accusés de réception sont désactivés No comment provided by engineer. + + Receive errors + Erreurs reçues + No comment provided by engineer. + Received at Reçu à @@ -4549,16 +6111,26 @@ Erreur : %@ Message reçu message info title + + 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. No comment provided by engineer. - - Receiving concurrency - Réception simultanée - No comment provided by engineer. - Receiving file will be stopped. La réception du fichier sera interrompue. @@ -4584,14 +6156,39 @@ Erreur : %@ 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 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? + Reconnecter les serveurs ? No comment provided by engineer. @@ -4609,10 +6206,23 @@ Erreur : %@ Réduction de la consommation de batterie No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject Rejeter - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4639,6 +6249,16 @@ Erreur : %@ 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 @@ -4666,7 +6286,7 @@ Erreur : %@ Renegotiate encryption? - Renégocier le chiffrement? + Renégocier le chiffrement ? No comment provided by engineer. @@ -4699,6 +6319,46 @@ Erreur : %@ Répondre chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required Requis @@ -4709,16 +6369,41 @@ Erreur : %@ 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 @@ -4759,9 +6444,9 @@ Erreur : %@ Révéler chat item action - - Revert - Revenir en arrière + + Review conditions + Vérifier les conditions No comment provided by engineer. @@ -4789,9 +6474,19 @@ Erreur : %@ Exécuter le chat No comment provided by engineer. - - SMP servers - Serveurs SMP + + SMP server + Serveur SMP + No comment provided by engineer. + + + SOCKS proxy + proxy SOCKS + No comment provided by engineer. + + + Safely receive files + Réception de fichiers en toute sécurité No comment provided by engineer. @@ -4802,43 +6497,43 @@ Erreur : %@ Save Enregistrer - chat item action + alert button +chat item action Save (and notify contacts) Enregistrer (et en informer les contacts) - No comment provided by engineer. + alert button Save and notify contact Enregistrer et en informer le contact - No comment provided by engineer. + alert button Save and notify group members Enregistrer et en informer les membres du groupe No comment provided by engineer. + + Save and reconnect + Sauvegarder et se reconnecter + No comment provided by engineer. + Save and update group profile Enregistrer et mettre à jour le profil du groupe No comment provided by engineer. - - Save archive - Enregistrer l'archive - No comment provided by engineer. - - - Save auto-accept settings - Enregistrer les paramètres de validation automatique - No comment provided by engineer. - Save group profile Enregistrer le profil du groupe No comment provided by engineer. + + Save list + No comment provided by engineer. + Save passphrase and open chat Enregistrer la phrase secrète et ouvrir le chat @@ -4852,7 +6547,7 @@ Erreur : %@ Save preferences? Enregistrer les préférences ? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6562,18 @@ Erreur : %@ Save servers? Enregistrer les serveurs ? - No comment provided by engineer. - - - Save settings? - Enregistrer les paramètres ? - No comment provided by engineer. + alert title Save welcome message? Enregistrer le message d'accueil ? No comment provided by engineer. + + Save your profile? + Sauvegarder votre profil ? + alert title + Saved Enregistré @@ -4899,6 +6594,21 @@ Erreur : %@ Message enregistré message info title + + 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 QR code Scanner un code QR @@ -4939,11 +6649,21 @@ Erreur : %@ Rechercher ou coller un lien SimpleX No comment provided by engineer. + + Secondary + Secondaire + No comment provided by engineer. + Secure queue File d'attente sécurisée server test step + + Secured + Sécurisées + No comment provided by engineer. + Security assessment Évaluation de sécurité @@ -4957,6 +6677,21 @@ Erreur : %@ Select Choisir + chat item action + + + 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. @@ -4994,11 +6729,6 @@ Erreur : %@ Envoyer les accusés de réception à 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 @@ -5009,9 +6739,14 @@ Erreur : %@ Envoyer un message éphémère No comment provided by engineer. + + Send errors + Erreurs d'envoi + No comment provided by engineer. + Send link previews - Envoi d'aperçus de liens + Aperçu des liens No comment provided by engineer. @@ -5019,14 +6754,28 @@ Erreur : %@ Envoyer un message dynamique No comment provided by engineer. + + Send message to enable calls. + Envoyer un message pour activer les appels. + No comment provided by engineer. + + + 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 notifications: - Envoi de notifications : + + Send private reports No comment provided by engineer. @@ -5052,7 +6801,7 @@ Erreur : %@ Sender cancelled file transfer. L'expéditeur a annulé le transfert de fichiers. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6858,11 @@ Erreur : %@ Envoyé le : %@ copied message info + + Sent directly + Envoyé directement + No comment provided by engineer. + Sent file event Événement de fichier envoyé @@ -5119,11 +6873,71 @@ Erreur : %@ Message envoyé message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + Serveur ajouté à l'opérateur %@. + alert message + + + 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 du réseau. + srv error text. + + + Server address is incompatible with network settings: %@. + L'adresse du serveur est incompatible avec les paramètres réseau : %@. + No comment provided by engineer. + + + Server operator changed. + L'opérateur du serveur a changé. + alert title + + + Server operators + Opérateurs de serveur + No comment provided by engineer. + + + Server protocol changed. + Le protocole du serveur a été modifié. + alert title + 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 @@ -5131,7 +6945,7 @@ Erreur : %@ Server requires authorization to upload, check password - Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe + Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe server test error @@ -5139,11 +6953,36 @@ Erreur : %@ Échec du test du serveur ! No comment provided by engineer. + + Server type + Type de serveur + No comment provided by engineer. + + + Server version is incompatible with network settings. + La version du serveur est incompatible avec les paramètres du réseau. + srv error text + + + 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 @@ -5154,11 +6993,20 @@ Erreur : %@ Définir 1 jour No comment provided by engineer. + + Set chat name… + 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 @@ -5169,6 +7017,10 @@ Erreur : %@ Il permet de remplacer l'authentification du système. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Définir le code d'accès @@ -5199,6 +7051,11 @@ Erreur : %@ Paramètres No comment provided by engineer. + + Settings were changed. + Les paramètres ont été modifiés. + alert message + Shape profile images Images de profil modelable @@ -5207,21 +7064,42 @@ Erreur : %@ Share Partager - chat item action + alert action +chat item action 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 SimpleX address on social media. + Partagez votre adresse SimpleX sur les réseaux sociaux. + No comment provided by engineer. + Share address Partager l'adresse No comment provided by engineer. + + Share address publicly + Partager publiquement votre adresse + No comment provided by engineer. + Share address with contacts? Partager l'adresse avec vos contacts ? + alert title + + + Share from other apps. + Partager depuis d'autres applications. No comment provided by engineer. @@ -5229,9 +7107,19 @@ Erreur : %@ Partager le lien No comment provided by engineer. + + Share profile + Partager le profil + No comment provided by engineer. + Share this 1-time invite link - Partager ce lien d'invitation unique + Partagez ce lien d'invitation unique + No comment provided by engineer. + + + Share to SimpleX + Partager sur SimpleX No comment provided by engineer. @@ -5239,6 +7127,10 @@ Erreur : %@ Partager avec vos contacts No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code Afficher le code QR @@ -5256,12 +7148,27 @@ Erreur : %@ Show last messages - Voir les derniers messages + Aperçu des derniers messages + No comment provided by engineer. + + + 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 - Afficher l'aperçu + Aperçu affiché + 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. @@ -5269,11 +7176,21 @@ Erreur : %@ Afficher : No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address Adresse SimpleX 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. @@ -5304,6 +7221,20 @@ Erreur : %@ 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 channel link + simplex link type + SimpleX contact address Adresse de contact SimpleX @@ -5324,8 +7255,8 @@ Erreur : %@ Liens SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. Les liens SimpleX sont interdits dans ce groupe. No comment provided by engineer. @@ -5339,11 +7270,21 @@ Erreur : %@ Invitation unique SimpleX simplex link type + + 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 @@ -5359,16 +7300,48 @@ Erreur : %@ Petits groupes (max 20) No comment provided by engineer. + + Soft + Léger + blur media + + + 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 : + No comment provided by engineer. + + + Some servers failed the test: +%@ + Certains serveurs ont échoué le test : +%@ + alert message + Somebody Quelqu'un notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. Carré, circulaire, ou toute autre forme intermédiaire. @@ -5389,6 +7362,16 @@ Erreur : %@ Démarrer la migration No comment provided by engineer. + + Starting from %@. + À partir de %@. + No comment provided by engineer. + + + Statistics + Statistiques + No comment provided by engineer. + Stop Arrêter @@ -5404,11 +7387,6 @@ Erreur : %@ Arrêter le chat 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 - 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é. @@ -5437,28 +7415,62 @@ Erreur : %@ Stop sharing Cesser le partage - No comment provided by engineer. + alert action Stop sharing address? Cesser le partage d'adresse ? - No comment provided by engineer. + alert title Stopping chat Arrêt du chat No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + Fort + blur media + 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 @@ -5469,11 +7481,20 @@ Erreur : %@ Authentification du système No comment provided by engineer. + + TCP connection + Connexion TCP + No comment provided by engineer. + TCP connection timeout Délai de connexion TCP No comment provided by engineer. + + TCP port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5489,11 +7510,21 @@ Erreur : %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Queue + No comment provided by engineer. + Take picture Prendre une photo 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 button Appuyez sur le bouton @@ -5529,16 +7560,20 @@ Erreur : %@ Appuyez pour scanner No comment provided by engineer. - - Tap to start a new chat - Appuyez ici pour démarrer une nouvelle discussion - No comment provided by engineer. + + Temporary file error + Erreur de fichier temporaire + file error alert title Test failed at step %@. Échec du test à l'étape %@. server test failure + + Test notifications + No comment provided by engineer. + Test server Tester le serveur @@ -5552,7 +7587,7 @@ Erreur : %@ Tests failed! Échec des tests ! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7604,6 @@ Erreur : %@ 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 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. @@ -5586,6 +7616,16 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. @@ -5596,6 +7636,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le code scanné n'est pas un code QR de lien SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, 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 ! @@ -5616,6 +7661,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le chiffrement fonctionne et le nouvel accord de chiffrement n'est pas nécessaire. Cela peut provoquer des erreurs de connexion ! No comment provided by engineer. + + The future of messaging + La nouvelle génération de messagerie privée + No comment provided by engineer. + The hash of the previous message is different. Le hash du message précédent est différent. @@ -5631,9 +7681,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. @@ -5641,9 +7696,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée. No comment provided by engineer. - - The profile is only shared with your contacts. - Le profil n'est partagé qu'avec vos contacts. + + 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. @@ -5661,14 +7721,29 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Les serveurs pour les nouvelles connexions de votre profil de chat actuel **%@**. No comment provided by engineer. + + 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. + The text you pasted is not a SimpleX link. Le texte collé n'est pas un lien SimpleX. No comment provided by engineer. - - Theme - Thème + + 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. @@ -5691,6 +7766,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + 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. @@ -5736,11 +7815,29 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Voici votre propre lien unique ! 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. + 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 message was deleted or not received yet. + 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 : @@ -5748,7 +7845,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. + Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l'app. No comment provided by engineer. @@ -5761,9 +7858,9 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. @@ -5771,6 +7868,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,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. 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. @@ -5788,26 +7910,60 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s 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. + + Token status: %@. + token status + + + 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 : %@). @@ -5863,10 +8019,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Débloquer ce membre ? No comment provided by engineer. - - Unexpected error: %@ - Erreur inattendue : %@ - item status description + + Undelivered messages + Messages non distribués + No comment provided by engineer. Unexpected migration state @@ -5876,7 +8032,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Unfav. Unfav. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +8069,11 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Erreur inconnue No comment provided by engineer. + + Unknown servers! + Serveurs inconnus ! + alert title + 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. @@ -5948,11 +8109,15 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Unmute Démute - No comment provided by engineer. + notification label action Unread Non lu + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5965,11 +8130,6 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Mise à jour No comment provided by engineer. - - Update .onion hosts setting? - Mettre à jour le paramètre des hôtes .onion ? - No comment provided by engineer. - Update database passphrase Mise à jour de la phrase secrète de la base de données @@ -5980,9 +8140,13 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 ? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5990,16 +8154,16 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 @@ -6007,24 +8171,52 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Upload file - Transférer le fichier + Téléverser le fichier server test step + + 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 %@ + Utiliser %@ + No comment provided by engineer. + Use .onion hosts Utiliser les hôtes .onions No comment provided by engineer. + + Use SOCKS proxy + Utiliser un proxy SOCKS + No comment provided by engineer. + Use SimpleX Chat servers? Utiliser les serveurs SimpleX Chat ? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Utiliser le chat @@ -6035,6 +8227,16 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 @@ -6060,24 +8262,52 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 short links (BETA) + No comment provided by engineer. + Use the app while in the call. Utiliser l'application pendant l'appel. No comment provided by engineer. - - User profile - Profil d'utilisateur + + Use the app with one hand. + Utiliser l'application d'une main. 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 web port + 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. @@ -6137,7 +8367,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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. + La vidéo ne sera reçue que lorsque votre contact aura fini la mettre en ligne. No comment provided by engineer. @@ -6150,11 +8380,21 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 + No comment provided by engineer. + Visible history Historique visible @@ -6170,8 +8410,8 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Les messages vocaux sont interdits dans ce chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Les messages vocaux sont interdits dans ce groupe. No comment provided by engineer. @@ -6210,9 +8450,19 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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. + 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 + 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. @@ -6255,9 +8505,9 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Lors des appels audio et vidéo. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Vous pouvez accepter ou refuser les demandes de contacts. + + 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. @@ -6295,24 +8545,39 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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. + No comment provided by engineer. + + + 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 : %@. + alert message + Wrong database passphrase Mauvaise phrase secrète pour la base de données No comment provided by engineer. + + 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. + snd 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é. + file error text + Wrong passphrase! Mauvaise phrase secrète ! No comment provided by engineer. - - XFTP servers - Serveurs XFTP - No comment provided by engineer. - - - You - Vous + + XFTP server + Serveur XFTP No comment provided by engineer. @@ -6340,6 +8605,11 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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 à %@. @@ -6387,11 +8657,26 @@ Répéter la demande d'adhésion ? 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 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 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 @@ -6422,11 +8707,21 @@ Répéter la demande d'adhésion ? Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Vous pouvez maintenant envoyer des messages à %@ notification body + + 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. @@ -6442,16 +8737,16 @@ Répéter la demande d'adhésion ? 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 la forme d'un lien ou d'un code QR - tout le monde peut l'utiliser pour vous contacter. - 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 No comment provided by engineer. + + 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. @@ -6465,23 +8760,23 @@ Répéter la demande d'adhésion ? You can view invitation link again in connection details. Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion. - No comment provided by engineer. + alert message You can't send messages! Vous ne pouvez pas envoyer de messages ! 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. - No comment provided by engineer. - You could not be verified; please try again. Vous n'avez pas pu être vérifié·e ; veuillez réessayer. No comment provided by engineer. + + You decide who can connect. + Vous choisissez qui peut se connecter. + No comment provided by engineer. + You have already requested connection via this address! Vous avez déjà demandé une connexion via cette adresse ! @@ -6494,11 +8789,6 @@ Repeat connection request? Ré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. @@ -6519,11 +8809,26 @@ Répéter la demande de connexion ? Vous avez rejoint ce groupe. Connexion à l'invitation d'un membre du groupe. 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. @@ -6539,6 +8844,10 @@ Répéter la demande de connexion ? Vous avez envoyé une invitation de groupe No comment provided by engineer. + + You should receive notifications. + token info + 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 ! @@ -6574,6 +8883,11 @@ Répéter la demande de connexion ? Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu'ils sont actifs. No comment provided by engineer. + + You will stop receiving messages from this 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é. @@ -6594,31 +8908,16 @@ Répéter la demande de connexion ? 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 ICE servers Vos serveurs ICE No comment provided by engineer. - - Your SMP servers - Vos serveurs SMP - No comment provided by engineer. - Your SimpleX address Votre adresse SimpleX No comment provided by engineer. - - Your XFTP servers - Vos serveurs XFTP - No comment provided by engineer. - Your calls Vos appels @@ -6634,16 +8933,19 @@ Répéter la demande de connexion ? Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète. No comment provided by engineer. + + Your chat preferences + Vos préférences de discussion + alert title + Your chat profiles Vos profils de chat No comment provided by engineer. - - 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). - 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). + + 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. @@ -6661,6 +8963,11 @@ Vous pouvez annuler la connexion et supprimer le contact (et réessayer plus tar Vos contacts resteront connectés. No comment provided by engineer. + + 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. @@ -6691,33 +8998,36 @@ Vous pouvez annuler la connexion et supprimer le contact (et réessayer plus tar Votre profil **%@** sera partagé. No comment provided by engineer. - - 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. + + 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. - - 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. + + 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. No comment provided by engineer. + + 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. + alert message + Your random profile Votre profil aléatoire No comment provided by engineer. - - Your server - Votre serveur - No comment provided by engineer. - Your server address Votre adresse de serveur No comment provided by engineer. + + Your servers + Vos serveurs + No comment provided by engineer. + Your settings Vos paramètres @@ -6758,6 +9068,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. appel accepté call status + + accepted invitation + invitation acceptée + chat list item title + admin admin @@ -6793,6 +9108,15 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. et %lld autres événements No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + tentatives + No comment provided by engineer. + audio call (not e2e encrypted) appel audio (sans chiffrement) @@ -6826,13 +9150,19 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. blocked by admin bloqué par l'administrateur - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold gras No comment provided by engineer. + + call + appeler + No comment provided by engineer. + call error erreur d'appel @@ -6936,7 +9266,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. connecting… connexion… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +9313,16 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. jours time unit + + decryption errors + Erreurs de déchiffrement + No comment provided by engineer. + default (%@) défaut (%@) - pref value + delete after time +pref value default (no) @@ -7033,6 +9369,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. message dupliqué integrity error chat item + + duplicates + doublons + No comment provided by engineer. + e2e encrypted chiffré de bout en bout @@ -7108,9 +9449,9 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. erreur No comment provided by engineer. - - event happened - event happened + + expired + expiré No comment provided by engineer. @@ -7143,6 +9484,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. 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. + + inactive + inactif + No comment provided by engineer. + incognito via contact address link mode incognito via le lien d'adresse du contact @@ -7183,6 +9529,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. invitation au groupe %@ group name + + invite + inviter + No comment provided by engineer. + invited invité·e @@ -7238,6 +9589,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. est connecté·e rcv group event chat item + + message + message + No comment provided by engineer. + message received message reçu @@ -7263,6 +9619,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. modéré par %@ marked deleted chat item preview text + + moderator + member role + months mois @@ -7271,7 +9631,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. never jamais - No comment provided by engineer. + delete after time new message @@ -7302,8 +9662,8 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. off off enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9680,16 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. on group pref value + + other + autre + No comment provided by engineer. + + + other errors + autres erreurs + No comment provided by engineer. + owner propriétaire @@ -7335,6 +9705,14 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. pair-à-pair No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption chiffrement e2e résistant post-quantique @@ -7350,6 +9728,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. confimation reçu… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call appel rejeté @@ -7380,6 +9762,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. vous a retiré rcv group event chat item + + requested to connect + demande à se connecter + chat list item title + saved enregistré @@ -7390,6 +9777,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. enregistré à partir de %@ No comment provided by engineer. + + search + rechercher + No comment provided by engineer. + sec sec @@ -7415,6 +9807,15 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. envoyer un message direct No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + info sur la file d'attente du serveur : %1$@ + +dernier message reçu : %2$@ + queue info + set new contact address a changé d'adresse de contact @@ -7455,11 +9856,21 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. inconnu connection info + + unknown servers + relais inconnus + No comment provided by engineer. + unknown status statut inconnu No comment provided by engineer. + + unprotected + non protégé + No comment provided by engineer. + updated group profile mise à jour du profil de groupe @@ -7500,6 +9911,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. via relais No comment provided by engineer. + + video + vidéo + No comment provided by engineer. + video call (not e2e encrypted) appel vidéo (sans chiffrement) @@ -7525,6 +9941,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. semaines time unit + + when IP hidden + lorsque l'IP est masquée + No comment provided by engineer. + yes oui @@ -7609,7 +10030,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.
- +
@@ -7646,7 +10067,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.
- +
@@ -7666,4 +10087,249 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.
+ +
+ +
+ + + %d new events + %d nouveaux événements + notification body + + + From %d chat(s) + notification body + + + From: %@ + De : %@ + notification body + + + New events + Nouveaux événements + notification + + + New messages + Nouveaux messages + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Tous droits réservés. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 45cfe0c468..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 ) No comment provided by engineer.
- - **Add new contact**: to create your one-time QR Code or link for your contact. - **הוסיפו איש קשר חדש**: ליצירת קוד QR או קישור חד־פעמיים עבור איש הקשר שלכם. - No comment provided by engineer. - **Create link / QR code** for your contact to use. **צור קישור / קוד QR** לשימוש איש הקשר שלך. 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. + + **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). + + **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. @@ -247,8 +242,8 @@ Available in v5.1 **שימו לב**: לא ניתן יהיה לשחזר או לשנות את הסיסמה אם תאבדו אותה. 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. + + **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. @@ -403,9 +398,9 @@ Available in v5.1 הוספת שרתים על ידי סריקת קוד QR. No comment provided by engineer. - - Add server… - הוסף שרת… + + Add server + הוסף שרת No comment provided by engineer. @@ -1391,8 +1386,8 @@ Available in v5.1 הודעות ישירות chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו. No comment provided by engineer. @@ -1411,8 +1406,8 @@ Available in v5.1 הודעות נעלמות אסורות בצ׳אט זה. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. הודעות נעלמות אסורות בקבוצה זו. No comment provided by engineer. @@ -1956,18 +1951,18 @@ Available in v5.1 חברי הקבוצה יכולים למחוק באופן בלתי הפיך הודעות שנשלחו. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. חברי הקבוצה יכולים לשלוח הודעות ישירות. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. חברי הקבוצה יכולים לשלוח הודעות נעלמות. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. חברי הקבוצה יכולים לשלוח הודעות קוליות. No comment provided by engineer. @@ -2115,8 +2110,8 @@ Available in v5.1 מיד No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam חסין מפני ספאם ושימוש לרעה No comment provided by engineer. @@ -2257,8 +2252,8 @@ Available in v5.1 מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו. No comment provided by engineer. @@ -2502,9 +2497,9 @@ Available in v5.1 ההעברה הושלמה No comment provided by engineer. - - Migrations: %@ - העברות: %@ + + Migrations: + העברות: No comment provided by engineer. @@ -2701,8 +2696,8 @@ Available in v5.1 לא ייעשה שימוש במארחי 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. @@ -2761,8 +2756,8 @@ Available in v5.1 Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2817,8 +2812,8 @@ Available in v5.1 Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -3521,8 +3516,8 @@ Available in v5.1 Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -3566,16 +3561,16 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of 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. @@ -3638,8 +3633,8 @@ It can happen because of some bug or when the connection is compromised.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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3864,8 +3859,8 @@ To connect, please ask your contact to create another connection link and check 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. @@ -4005,10 +4000,6 @@ SimpleX Lock must be enabled. You can't send messages! 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. @@ -4967,8 +4958,8 @@ SimpleX servers cannot see your profile. נמחק No comment provided by engineer. - - Files and media are prohibited in this group. + + Files and media are prohibited. קבצים ומדיה אסורים בקבוצה זו. No comment provided by engineer. @@ -5027,13 +5018,13 @@ SimpleX servers cannot see your profile. הזמן חברים No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. חברי הקבוצה יכולים לשלוח קבצים ומדיה. No comment provided by engineer. @@ -5231,8 +5222,8 @@ SimpleX servers cannot see your profile. תגובות אמוג׳י להודעות אסורות בצ׳אט זה. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. תגובות אמוג׳י להודעות אסורות בקבוצה זו. No comment provided by engineer. @@ -5316,6 +5307,278 @@ SimpleX servers cannot see your profile. %@ ו-%@ No comment provided by engineer. + + Connect automatically + התבר אוטומטי + + + Create profile + צור פרופיל + + + Created at: %@ + נוצר ב:%@ + + + Desktop devices + מכשירי מחשב + + + Discover via local network + גלה באמצעות הרשת המקומית + + + Forward + העבר + + + Group already exists + קבוצה כבר קיימת + + + Connected to desktop + מחובר למחשב + + + Group already exists! + קבוצה כבר קיימת! + + + Confirm upload + אשר ההעלאה + + + Block for all + חסום לכולם + + + Blocked by admin + נחסם ע"י מנהל + + + Block member for all? + לחסום את החבר לכולם? + + + Camera not available + מצלמה לא זמינה + + + Connect to desktop + חבר למחשב + + + Created at + נוצר ב + + + (new) + (חדש) + + + Block member + חבר חסום + + + Block member? + לחסום חבר? + + + Creating link… + יוצר קישור… + + + Files + קבצים + + + Disabled + מושבת + + + Enter passphrase + הכנס סיסמא + + + Apply + החל + + + Apply to + החל ל + + + Background + ברקע + + + Black + שחור + + + Blur media + טשטש מדיה + + + Chat theme + צבע ערכת נושא + + + Completed + הושלם + + + Connected + מחובר + + + Connection notifications + התראות חיבור + + + Connections + חיבורים + + + Current profile + פרופיל נוכחי + + + Disconnect desktop? + להתנתק מהמחשב? + + + Discover and join groups + גלה והצטרף לקבוצות + + + Enabled + מופעל + + + Error opening chat + שגיאה בפתיחת הצ'אט + + + Good morning! + בוקר טוב! + + + Connect to yourself? +This is your own SimpleX address! + להתחבר אליך? +זו כתובת הSimpleX שלך! + + + Connect to yourself? + להתחבר אליך? + + + Connect to yourself? +This is your own one-time link! + להתחבר אליך? +זו כתובת ההזמנה החד-פעמי שלך! + + + Connected desktop + מחשב מחובר + + + Connected servers + שרתים מחוברים + + + Enter group name… + הכנס שם לקבוצה… + + + Enter this device name… + הכנס שם למכשיר הזה… + + + Enter your name… + הכנס את השם שלך… + + + Error decrypting file + שגיאה בפענוח הקובץ + + + Errors + שגיאות + + + File status + מצב הקובץ + + + Connecting + מתחבר + + + Connecting to desktop + מתחבר למחשב + + + Deleted + נמחק + + + Deletion errors + שגיאות במחיקה + + + Details + פרטים + + + Forwarded + הועבר + + + Found desktop + נמצא מחשב + + + Good afternoon! + אחר צהריים טובים! + + + Desktop address + כתובת מחשב + + + Forwarded from + הועבר מ + + + History is not sent to new members. + היסטוריה לא נשלחת לחברים חדשים. + + + Created + נוצר + + + Copy error + שגיאת העתקה + + + Create group + צור קבוצה + + + Enabled for + מופעל עבור + + + Error creating message + שגיאה ביצירת הודעה + + + File error + שגיאה בקובץ + 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 @@ - - - -
- -
- - - - - 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. - - - !1 colored! - No comment provided by engineer. - - - #secret# - No comment provided by engineer. - - - %@ - No comment provided by engineer. - - - %@ %@ - No comment provided by engineer. - - - %@ / %@ - No comment provided by engineer. - - - %@ is connected! - notification title - - - %@ is not verified - No comment provided by engineer. - - - %@ is verified - No comment provided by engineer. - - - %@ wants to connect! - notification title - - - %d days - message ttl - - - %d hours - message ttl - - - %d min - message ttl - - - %d months - message ttl - - - %d sec - message ttl - - - %d skipped message(s) - integrity error chat item - - - %lld - No comment provided by engineer. - - - %lld %@ - No comment provided by engineer. - - - %lld contact(s) selected - No comment provided by engineer. - - - %lld file(s) with total size of %@ - No comment provided by engineer. - - - %lld members - No comment provided by engineer. - - - %lld second(s) - No comment provided by engineer. - - - %lldd - No comment provided by engineer. - - - %lldh - No comment provided by engineer. - - - %lldk - No comment provided by engineer. - - - %lldm - No comment provided by engineer. - - - %lldmth - No comment provided by engineer. - - - %llds - No comment provided by engineer. - - - %lldw - No comment provided by engineer. - - - ( - No comment provided by engineer. - - - ) - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - - - **Create link / QR code** for your contact to use. - 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. - 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). - No comment provided by engineer. - - - **Paste received link** or open it in the browser and tap **Open in mobile app**. - No comment provided by engineer. - - - **Please note**: you will NOT be able to recover or change passphrase if you lose it. - No comment provided by engineer. - - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. - No comment provided by engineer. - - - **Scan QR code**: to connect to your contact in person or via video call. - No comment provided by engineer. - - - **Warning**: Instant push notifications require passphrase saved in Keychain. - No comment provided by engineer. - - - **e2e encrypted** audio call - No comment provided by engineer. - - - **e2e encrypted** video call - No comment provided by engineer. - - - \*bold* - No comment provided by engineer. - - - , - No comment provided by engineer. - - - . - No comment provided by engineer. - - - 1 day - message ttl - - - 1 hour - message ttl - - - 1 month - message ttl - - - 1 week - message ttl - - - 2 weeks - message ttl - - - 6 - No comment provided by engineer. - - - : - No comment provided by engineer. - - - A new contact - notification title - - - A random profile will be sent to the contact that you received this link from - No comment provided by engineer. - - - A random profile will be sent to your contact - No comment provided by engineer. - - - A separate TCP connection will be used **for each chat profile you have in the app**. - No comment provided by engineer. - - - 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. - No comment provided by engineer. - - - About SimpleX - No comment provided by engineer. - - - About SimpleX Chat - No comment provided by engineer. - - - Accent color - No comment provided by engineer. - - - Accept - accept contact request via notification - accept incoming call via notification - - - Accept contact - No comment provided by engineer. - - - Accept contact request from %@? - notification body - - - Accept incognito - No comment provided by engineer. - - - Accept requests - No comment provided by engineer. - - - Add preset servers - No comment provided by engineer. - - - Add profile - No comment provided by engineer. - - - Add servers by scanning QR codes. - No comment provided by engineer. - - - Add server… - No comment provided by engineer. - - - Add to another device - 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. - - - All chats and messages will be deleted - this cannot be undone! - No comment provided by engineer. - - - All group members will remain connected. - No comment provided by engineer. - - - All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - No comment provided by engineer. - - - All your contacts will remain connected - No comment provided by engineer. - - - Allow - No comment provided by engineer. - - - Allow disappearing messages only if your contact allows it to you. - No comment provided by engineer. - - - Allow irreversible message deletion only if your contact allows it to you. - No comment provided by engineer. - - - Allow sending direct messages to members. - No comment provided by engineer. - - - Allow sending disappearing messages. - No comment provided by engineer. - - - Allow to irreversibly delete sent messages. - No comment provided by engineer. - - - Allow to send voice messages. - No comment provided by engineer. - - - Allow voice messages only if your contact allows them. - No comment provided by engineer. - - - Allow voice messages? - No comment provided by engineer. - - - Allow your contacts to irreversibly delete sent messages. - No comment provided by engineer. - - - Allow your contacts to send disappearing messages. - No comment provided by engineer. - - - Allow your contacts to send voice messages. - No comment provided by engineer. - - - Already connected? - No comment provided by engineer. - - - Answer call - No comment provided by engineer. - - - App build: %@ - No comment provided by engineer. - - - App icon - No comment provided by engineer. - - - App version - No comment provided by engineer. - - - App version: v%@ - No comment provided by engineer. - - - Appearance - No comment provided by engineer. - - - Attach - No comment provided by engineer. - - - Audio & video calls - No comment provided by engineer. - - - Authentication failed - No comment provided by engineer. - - - Authentication unavailable - No comment provided by engineer. - - - Auto-accept contact requests - No comment provided by engineer. - - - Auto-accept images - No comment provided by engineer. - - - Automatically - No comment provided by engineer. - - - Back - No comment provided by engineer. - - - Both you and your contact can irreversibly delete sent messages. - No comment provided by engineer. - - - Both you and your contact can send disappearing messages. - No comment provided by engineer. - - - Both you and your contact can send voice messages. - No comment provided by engineer. - - - Call already ended! - No comment provided by engineer. - - - Calls - No comment provided by engineer. - - - Can't invite contact! - No comment provided by engineer. - - - Can't invite contacts! - No comment provided by engineer. - - - Cancel - No comment provided by engineer. - - - Cannot access keychain to save database password - No comment provided by engineer. - - - Cannot receive file - No comment provided by engineer. - - - Change - No comment provided by engineer. - - - Change database passphrase? - No comment provided by engineer. - - - Change member role? - No comment provided by engineer. - - - Change receiving address - No comment provided by engineer. - - - Change receiving address? - No comment provided by engineer. - - - Change role - No comment provided by engineer. - - - Chat archive - No comment provided by engineer. - - - Chat console - No comment provided by engineer. - - - Chat database - No comment provided by engineer. - - - Chat database deleted - No comment provided by engineer. - - - Chat database imported - No comment provided by engineer. - - - Chat is running - No comment provided by engineer. - - - Chat is stopped - No comment provided by engineer. - - - Chat preferences - No comment provided by engineer. - - - Chats - No comment provided by engineer. - - - Check server address and try again. - No comment provided by engineer. - - - Choose file - No comment provided by engineer. - - - Choose from library - No comment provided by engineer. - - - Clear - No comment provided by engineer. - - - Clear conversation - No comment provided by engineer. - - - Clear conversation? - No comment provided by engineer. - - - Clear verification - No comment provided by engineer. - - - Colors - No comment provided by engineer. - - - Compare security codes with your contacts. - No comment provided by engineer. - - - Configure ICE servers - No comment provided by engineer. - - - Confirm - No comment provided by engineer. - - - Confirm new passphrase… - No comment provided by engineer. - - - Connect - server test step - - - Connect via contact link? - No comment provided by engineer. - - - Connect via group link? - No comment provided by engineer. - - - Connect via link - No comment provided by engineer. - - - Connect via link / QR code - No comment provided by engineer. - - - Connect via one-time link? - No comment provided by engineer. - - - Connect via relay - No comment provided by engineer. - - - Connecting to server… - No comment provided by engineer. - - - Connecting to server… (error: %@) - No comment provided by engineer. - - - Connection - No comment provided by engineer. - - - Connection error - No comment provided by engineer. - - - Connection error (AUTH) - No comment provided by engineer. - - - Connection request - No comment provided by engineer. - - - Connection request sent! - No comment provided by engineer. - - - Connection timeout - No comment provided by engineer. - - - Contact allows - No comment provided by engineer. - - - Contact already exists - No comment provided by engineer. - - - Contact and all messages will be deleted - this cannot be undone! - No comment provided by engineer. - - - Contact hidden: - notification - - - Contact is connected - notification - - - Contact is not connected yet! - No comment provided by engineer. - - - Contact name - No comment provided by engineer. - - - Contact preferences - No comment provided by engineer. - - - Contact requests - No comment provided by engineer. - - - Contacts can mark messages for deletion; you will be able to view them. - No comment provided by engineer. - - - Copy - chat item action - - - Core built at: %@ - No comment provided by engineer. - - - Core version: v%@ - No comment provided by engineer. - - - Create - No comment provided by engineer. - - - Create address - No comment provided by engineer. - - - Create group link - No comment provided by engineer. - - - Create link - No comment provided by engineer. - - - Create one-time invitation link - No comment provided by engineer. - - - Create queue - server test step - - - Create secret group - No comment provided by engineer. - - - Create your profile - No comment provided by engineer. - - - Created on %@ - No comment provided by engineer. - - - Current passphrase… - No comment provided by engineer. - - - Currently maximum supported file size is %@. - No comment provided by engineer. - - - Dark - No comment provided by engineer. - - - Data - No comment provided by engineer. - - - Database ID - No comment provided by engineer. - - - Database encrypted! - No comment provided by engineer. - - - Database encryption passphrase will be updated and stored in the keychain. - - No comment provided by engineer. - - - Database encryption passphrase will be updated. - - No comment provided by engineer. - - - Database error - No comment provided by engineer. - - - Database is encrypted using a random passphrase, you can change it. - No comment provided by engineer. - - - Database is encrypted using a random passphrase. Please change it before exporting. - No comment provided by engineer. - - - Database passphrase - No comment provided by engineer. - - - Database passphrase & export - No comment provided by engineer. - - - Database passphrase is different from saved in the keychain. - No comment provided by engineer. - - - Database passphrase is required to open chat. - No comment provided by engineer. - - - Database will be encrypted and the passphrase stored in the keychain. - - No comment provided by engineer. - - - Database will be encrypted. - - No comment provided by engineer. - - - Database will be migrated when the app restarts - No comment provided by engineer. - - - Decentralized - No comment provided by engineer. - - - Delete - chat item action - - - Delete Contact - No comment provided by engineer. - - - Delete address - No comment provided by engineer. - - - Delete address? - No comment provided by engineer. - - - Delete after - No comment provided by engineer. - - - Delete all files - No comment provided by engineer. - - - Delete archive - No comment provided by engineer. - - - Delete chat archive? - No comment provided by engineer. - - - Delete chat profile? - No comment provided by engineer. - - - Delete connection - No comment provided by engineer. - - - Delete contact - No comment provided by engineer. - - - Delete contact? - No comment provided by engineer. - - - Delete database - No comment provided by engineer. - - - Delete files & media - No comment provided by engineer. - - - Delete files and media? - No comment provided by engineer. - - - Delete files for all chat profiles - No comment provided by engineer. - - - Delete for everyone - chat feature - - - Delete for me - No comment provided by engineer. - - - Delete group - No comment provided by engineer. - - - Delete group? - No comment provided by engineer. - - - Delete invitation - No comment provided by engineer. - - - Delete link - No comment provided by engineer. - - - Delete link? - No comment provided by engineer. - - - Delete message? - No comment provided by engineer. - - - Delete messages - No comment provided by engineer. - - - Delete messages after - No comment provided by engineer. - - - Delete old database - No comment provided by engineer. - - - Delete old database? - No comment provided by engineer. - - - Delete pending connection - No comment provided by engineer. - - - Delete pending connection? - No comment provided by engineer. - - - Delete queue - server test step - - - Delete user profile? - No comment provided by engineer. - - - Description - No comment provided by engineer. - - - Develop - No comment provided by engineer. - - - Developer tools - No comment provided by engineer. - - - Device - No comment provided by engineer. - - - Device authentication is disabled. Turning off SimpleX Lock. - No comment provided by engineer. - - - Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. - No comment provided by engineer. - - - Direct messages - chat feature - - - Direct messages between members are prohibited in this group. - No comment provided by engineer. - - - Disable SimpleX Lock - authentication reason - - - Disappearing messages - chat feature - - - Disappearing messages are prohibited in this chat. - No comment provided by engineer. - - - Disappearing messages are prohibited in this group. - No comment provided by engineer. - - - Disconnect - server test step - - - Display name - No comment provided by engineer. - - - Display name: - No comment provided by engineer. - - - Do NOT use SimpleX for emergency calls. - No comment provided by engineer. - - - Do it later - No comment provided by engineer. - - - Edit - chat item action - - - Edit group profile - No comment provided by engineer. - - - Enable - No comment provided by engineer. - - - Enable SimpleX Lock - authentication reason - - - Enable TCP keep-alive - No comment provided by engineer. - - - Enable automatic message deletion? - No comment provided by engineer. - - - Enable instant notifications? - No comment provided by engineer. - - - Enable notifications - No comment provided by engineer. - - - Enable periodic notifications? - No comment provided by engineer. - - - Encrypt - No comment provided by engineer. - - - Encrypt database? - No comment provided by engineer. - - - Encrypted database - No comment provided by engineer. - - - Encrypted message or another event - notification - - - Encrypted message: database error - notification - - - Encrypted message: keychain error - notification - - - Encrypted message: no passphrase - notification - - - Encrypted message: unexpected error - notification - - - Enter correct passphrase. - No comment provided by engineer. - - - Enter passphrase… - No comment provided by engineer. - - - Enter server manually - No comment provided by engineer. - - - Error - No comment provided by engineer. - - - Error accepting contact request - No comment provided by engineer. - - - Error accessing database file - No comment provided by engineer. - - - Error adding member(s) - No comment provided by engineer. - - - Error changing address - No comment provided by engineer. - - - Error changing role - No comment provided by engineer. - - - Error changing setting - No comment provided by engineer. - - - Error creating address - No comment provided by engineer. - - - Error creating group - No comment provided by engineer. - - - Error creating group link - No comment provided by engineer. - - - Error deleting chat database - No comment provided by engineer. - - - Error deleting chat! - No comment provided by engineer. - - - Error deleting connection - No comment provided by engineer. - - - Error deleting contact - No comment provided by engineer. - - - Error deleting database - No comment provided by engineer. - - - Error deleting old database - No comment provided by engineer. - - - Error deleting token - No comment provided by engineer. - - - Error deleting user profile - No comment provided by engineer. - - - Error enabling notifications - No comment provided by engineer. - - - Error encrypting database - No comment provided by engineer. - - - Error exporting chat database - No comment provided by engineer. - - - Error importing chat database - No comment provided by engineer. - - - Error joining group - No comment provided by engineer. - - - Error receiving file - No comment provided by engineer. - - - Error removing member - No comment provided by engineer. - - - Error saving ICE servers - No comment provided by engineer. - - - Error saving SMP servers - No comment provided by engineer. - - - Error saving group profile - No comment provided by engineer. - - - Error saving passphrase to keychain - No comment provided by engineer. - - - Error sending message - No comment provided by engineer. - - - Error starting chat - No comment provided by engineer. - - - Error stopping chat - No comment provided by engineer. - - - Error updating message - No comment provided by engineer. - - - Error updating settings - No comment provided by engineer. - - - Error: %@ - No comment provided by engineer. - - - Error: URL is invalid - No comment provided by engineer. - - - Error: no database file - No comment provided by engineer. - - - Exit without saving - No comment provided by engineer. - - - Export database - No comment provided by engineer. - - - Export error: - No comment provided by engineer. - - - Exported database archive. - No comment provided by engineer. - - - Exporting database archive... - No comment provided by engineer. - - - Failed to remove passphrase - No comment provided by engineer. - - - File will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - File: %@ - No comment provided by engineer. - - - Files & media - No comment provided by engineer. - - - For console - No comment provided by engineer. - - - Full link - No comment provided by engineer. - - - Full name (optional) - No comment provided by engineer. - - - Full name: - No comment provided by engineer. - - - GIFs and stickers - No comment provided by engineer. - - - Group - No comment provided by engineer. - - - Group display name - No comment provided by engineer. - - - Group full name (optional) - No comment provided by engineer. - - - Group image - No comment provided by engineer. - - - Group invitation - No comment provided by engineer. - - - Group invitation expired - No comment provided by engineer. - - - Group invitation is no longer valid, it was removed by sender. - No comment provided by engineer. - - - Group link - No comment provided by engineer. - - - Group links - No comment provided by engineer. - - - Group members can 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 voice messages. - No comment provided by engineer. - - - Group message: - notification - - - Group preferences - No comment provided by engineer. - - - Group profile - No comment provided by engineer. - - - Group profile is stored on members' devices, not on the servers. - No comment provided by engineer. - - - Group will be deleted for all members - this cannot be undone! - No comment provided by engineer. - - - Group will be deleted for you - this cannot be undone! - No comment provided by engineer. - - - Help - No comment provided by engineer. - - - Hidden - No comment provided by engineer. - - - Hide - chat item action - - - Hide app screen in the recent apps. - No comment provided by engineer. - - - How SimpleX works - No comment provided by engineer. - - - How it works - No comment provided by engineer. - - - How to - No comment provided by engineer. - - - How to use it - No comment provided by engineer. - - - How to use your servers - No comment provided by engineer. - - - ICE servers (one per line) - No comment provided by engineer. - - - If the video fails to connect, flip the camera to resolve it. - No comment provided by engineer. - - - If you can't meet in person, **show QR code in the 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 need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). - No comment provided by engineer. - - - Ignore - No comment provided by engineer. - - - Image will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - Immune to spam and abuse - No comment provided by engineer. - - - Import - No comment provided by engineer. - - - Import chat database? - No comment provided by engineer. - - - Import database - No comment provided by engineer. - - - Improved privacy and security - No comment provided by engineer. - - - Improved server configuration - No comment provided by engineer. - - - Incognito - No comment provided by engineer. - - - Incognito mode - No comment provided by engineer. - - - Incognito mode is not supported here - your main profile will be sent to group members - No comment provided by engineer. - - - Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created. - No comment provided by engineer. - - - Incoming audio call - notification - - - Incoming call - notification - - - Incoming video call - notification - - - Incorrect security code! - No comment provided by engineer. - - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - - - Instant push notifications will be hidden! - - No comment provided by engineer. - - - Instantly - No comment provided by engineer. - - - Invalid connection link - No comment provided by engineer. - - - Invalid server address! - No comment provided by engineer. - - - Invitation expired! - No comment provided by engineer. - - - Invite members - No comment provided by engineer. - - - Invite to group - No comment provided by engineer. - - - Irreversible message deletion - No comment provided by engineer. - - - Irreversible message deletion is prohibited in this chat. - No comment provided by engineer. - - - Irreversible message deletion is prohibited in this group. - No comment provided by engineer. - - - It allows having many anonymous connections without any shared data between them in a single chat profile. - No comment provided by engineer. - - - It can happen when: -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. - No comment provided by engineer. - - - It seems like you are already connected via this link. If it is not the case, there was an error (%@). - No comment provided by engineer. - - - Join - No comment provided by engineer. - - - Join group - No comment provided by engineer. - - - Join incognito - No comment provided by engineer. - - - Joining group - No comment provided by engineer. - - - Keychain error - No comment provided by engineer. - - - LIVE - No comment provided by engineer. - - - Large file! - No comment provided by engineer. - - - Leave - No comment provided by engineer. - - - Leave group - No comment provided by engineer. - - - Leave group? - No comment provided by engineer. - - - Light - No comment provided by engineer. - - - Limitations - No comment provided by engineer. - - - Live message! - No comment provided by engineer. - - - Live messages - No comment provided by engineer. - - - Local name - No comment provided by engineer. - - - Local profile data only - No comment provided by engineer. - - - Make a private connection - No comment provided by engineer. - - - Make sure SMP 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. - No comment provided by engineer. - - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - No comment provided by engineer. - - - Mark deleted for everyone - No comment provided by engineer. - - - Mark read - No comment provided by engineer. - - - Mark verified - No comment provided by engineer. - - - Markdown in messages - No comment provided by engineer. - - - Max 30 seconds, received instantly. - No comment provided by engineer. - - - Member - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - No comment provided by engineer. - - - Member will be removed from group - this cannot be undone! - No comment provided by engineer. - - - Message delivery error - No comment provided by engineer. - - - Message text - No comment provided by engineer. - - - Messages - No comment provided by engineer. - - - Migrating database archive... - No comment provided by engineer. - - - Migration error: - No comment provided by engineer. - - - Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). - No comment provided by engineer. - - - Migration is completed - No comment provided by engineer. - - - Most likely this contact has deleted the connection with you. - No comment provided by engineer. - - - Mute - No comment provided by engineer. - - - Name - No comment provided by engineer. - - - Network & servers - No comment provided by engineer. - - - Network settings - No comment provided by engineer. - - - Network status - No comment provided by engineer. - - - New contact request - notification - - - New contact: - notification - - - New database archive - No comment provided by engineer. - - - New in %@ - No comment provided by engineer. - - - New member role - No comment provided by engineer. - - - New message - notification - - - New passphrase… - No comment provided by engineer. - - - No - No comment provided by engineer. - - - No contacts selected - No comment provided by engineer. - - - No contacts to add - No comment provided by engineer. - - - No device token! - No comment provided by engineer. - - - Group not found! - No comment provided by engineer. - - - No permission to record voice message - No comment provided by engineer. - - - No received or sent files - No comment provided by engineer. - - - Notifications - No comment provided by engineer. - - - Notifications are disabled! - No comment provided by engineer. - - - Off (Local) - No comment provided by engineer. - - - Ok - No comment provided by engineer. - - - Old database - No comment provided by engineer. - - - Old database archive - No comment provided by engineer. - - - One-time invitation link - No comment provided by engineer. - - - Onion hosts will be required for connection. Requires enabling VPN. - No comment provided by engineer. - - - Onion hosts will be used when available. Requires enabling 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**. - No comment provided by engineer. - - - Only group owners can change group preferences. - No comment provided by engineer. - - - Only group owners can enable voice messages. - No comment provided by engineer. - - - Only you can irreversibly delete messages (your contact can mark them for deletion). - No comment provided by engineer. - - - Only you can send disappearing messages. - No comment provided by engineer. - - - Only you can send voice messages. - No comment provided by engineer. - - - Only your contact can irreversibly delete messages (you can mark them for deletion). - No comment provided by engineer. - - - Only your contact can send disappearing messages. - No comment provided by engineer. - - - Only your contact can send voice messages. - No comment provided by engineer. - - - Open Settings - No comment provided by engineer. - - - Open chat - No comment provided by engineer. - - - Open chat console - authentication reason - - - Open-source protocol and code – anybody can run the servers. - No comment provided by engineer. - - - Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red. - No comment provided by engineer. - - - PING count - No comment provided by engineer. - - - PING interval - 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. - - - Paste the link you received into the box below to connect with your contact. - No comment provided by engineer. - - - People can connect to you only via the links you share. - No comment provided by engineer. - - - Periodically - No comment provided by engineer. - - - Please ask your contact to enable sending voice messages. - No comment provided by engineer. - - - Please check that you used the correct link or ask your contact to send you another one. - No comment provided by engineer. - - - Please check your network connection with %@ and try again. - No comment provided by engineer. - - - Please check yours and your contact preferences. - No comment provided by engineer. - - - Please enter correct current passphrase. - No comment provided by engineer. - - - Please enter the previous password after restoring database backup. This action can not be undone. - No comment provided by engineer. - - - Please restart the app and migrate the database to enable push notifications. - No comment provided by engineer. - - - Please store passphrase securely, you will NOT be able to access chat if you lose it. - No comment provided by engineer. - - - Please store passphrase securely, you will NOT be able to change it if you lose it. - No comment provided by engineer. - - - Possibly, certificate fingerprint in server address is incorrect - server test error - - - Preset server - No comment provided by engineer. - - - Preset server address - No comment provided by engineer. - - - Privacy & security - No comment provided by engineer. - - - Privacy redefined - No comment provided by engineer. - - - Profile and server connections - No comment provided by engineer. - - - Profile image - No comment provided by engineer. - - - Prohibit irreversible message deletion. - No comment provided by engineer. - - - Prohibit sending direct messages to members. - No comment provided by engineer. - - - Prohibit sending disappearing messages. - No comment provided by engineer. - - - Prohibit sending voice messages. - No comment provided by engineer. - - - Protect app screen - No comment provided by engineer. - - - Protocol timeout - No comment provided by engineer. - - - Push notifications - No comment provided by engineer. - - - Rate the app - No comment provided by engineer. - - - Read - No comment provided by engineer. - - - Read more in our GitHub repository. - No comment provided by engineer. - - - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - No comment provided by engineer. - - - Received file event - notification - - - Receiving via - No comment provided by engineer. - - - Recipients see updates as you type them. - No comment provided by engineer. - - - Reject - reject incoming call via notification - - - Reject contact (sender NOT notified) - No comment provided by engineer. - - - Reject contact request - No comment provided by engineer. - - - Relay server is only used if necessary. Another party can observe your IP address. - No comment provided by engineer. - - - Relay server protects your IP address, but it can observe the duration of the call. - No comment provided by engineer. - - - Remove - No comment provided by engineer. - - - Remove member - No comment provided by engineer. - - - Remove member? - No comment provided by engineer. - - - Remove passphrase from keychain? - No comment provided by engineer. - - - Reply - chat item action - - - Required - No comment provided by engineer. - - - Reset - No comment provided by engineer. - - - Reset colors - No comment provided by engineer. - - - Reset to defaults - No comment provided by engineer. - - - Restart the app to create a new chat profile - No comment provided by engineer. - - - Restart the app to use imported chat database - No comment provided by engineer. - - - Restore - No comment provided by engineer. - - - Restore database backup - No comment provided by engineer. - - - Restore database backup? - No comment provided by engineer. - - - Restore database error - No comment provided by engineer. - - - Reveal - chat item action - - - Revert - No comment provided by engineer. - - - Role - No comment provided by engineer. - - - Run chat - No comment provided by engineer. - - - SMP servers - No comment provided by engineer. - - - Save - chat item action - - - Save (and notify contacts) - No comment provided by engineer. - - - Save and notify contact - No comment provided by engineer. - - - Save and notify group members - No comment provided by engineer. - - - Save archive - No comment provided by engineer. - - - Save group profile - No comment provided by engineer. - - - Save passphrase and open chat - No comment provided by engineer. - - - Save passphrase in Keychain - No comment provided by engineer. - - - Save preferences? - No comment provided by engineer. - - - Save servers - No comment provided by engineer. - - - Saved WebRTC ICE servers will be removed - No comment provided by engineer. - - - Scan QR code - No comment provided by engineer. - - - Scan code - No comment provided by engineer. - - - Scan security code from your contact's app. - No comment provided by engineer. - - - Scan server QR code - No comment provided by engineer. - - - Search - No comment provided by engineer. - - - Secure queue - server test step - - - Security assessment - No comment provided by engineer. - - - Security code - No comment provided by engineer. - - - Send - No comment provided by engineer. - - - Send a live message - it will update for the recipient(s) as you type it - No comment provided by engineer. - - - Send direct message - No comment provided by engineer. - - - Send link previews - No comment provided by engineer. - - - Send live message - No comment provided by engineer. - - - Send notifications - No comment provided by engineer. - - - Send notifications: - No comment provided by engineer. - - - Send questions and ideas - No comment provided by engineer. - - - Send them from gallery or custom keyboards. - No comment provided by engineer. - - - Sender cancelled file transfer. - No comment provided by engineer. - - - Sender may have deleted the connection request. - No comment provided by engineer. - - - Sending via - No comment provided by engineer. - - - Sent file event - notification - - - Sent messages will be deleted after set time. - No comment provided by engineer. - - - Server requires authorization to create queues, check password - server test error - - - Server test failed! - No comment provided by engineer. - - - Servers - No comment provided by engineer. - - - Set 1 day - No comment provided by engineer. - - - Set contact name… - No comment provided by engineer. - - - Set group preferences - No comment provided by engineer. - - - Set passphrase to export - No comment provided by engineer. - - - Set timeouts for proxy/VPN - No comment provided by engineer. - - - Settings - No comment provided by engineer. - - - Share - chat item action - - - Share invitation link - No comment provided by engineer. - - - Share link - No comment provided by engineer. - - - Share one-time invitation link - No comment provided by engineer. - - - Show QR code - No comment provided by engineer. - - - Show preview - No comment provided by engineer. - - - SimpleX Chat security was audited by Trail of Bits. - No comment provided by engineer. - - - SimpleX Lock - No comment provided by engineer. - - - SimpleX Lock turned on - No comment provided by engineer. - - - SimpleX contact address - simplex link type - - - SimpleX encrypted message or connection event - notification - - - SimpleX group link - simplex link type - - - SimpleX links - No comment provided by engineer. - - - SimpleX one-time invitation - simplex link type - - - Skip - No comment provided by engineer. - - - Skipped messages - No comment provided by engineer. - - - Somebody - notification title - - - Start a new chat - No comment provided by engineer. - - - Start chat - No comment provided by engineer. - - - Start migration - No comment provided by engineer. - - - Stop - No comment provided by engineer. - - - Stop SimpleX - authentication reason - - - Stop chat to enable database actions - No comment provided by engineer. - - - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - No comment provided by engineer. - - - Stop chat? - No comment provided by engineer. - - - Support SimpleX Chat - No comment provided by engineer. - - - System - No comment provided by engineer. - - - TCP connection timeout - No comment provided by engineer. - - - TCP_KEEPCNT - No comment provided by engineer. - - - TCP_KEEPIDLE - No comment provided by engineer. - - - TCP_KEEPINTVL - No comment provided by engineer. - - - Take picture - No comment provided by engineer. - - - Tap button - No comment provided by engineer. - - - Tap to join - No comment provided by engineer. - - - Tap to join incognito - No comment provided by engineer. - - - Tap to start a new chat - No comment provided by engineer. - - - Test failed at step %@. - server test failure - - - Test server - No comment provided by engineer. - - - Test servers - No comment provided by engineer. - - - Tests failed! - No comment provided by engineer. - - - Thank you for installing SimpleX Chat! - No comment provided by engineer. - - - The 1st platform without any user identifiers – private by design. - No comment provided by engineer. - - - The app can notify you when you receive messages or contact requests - please open settings to enable. - No comment provided by engineer. - - - The attempt to change database passphrase was not completed. - No comment provided by engineer. - - - The connection you accepted will be cancelled! - No comment provided by engineer. - - - The contact you shared this link with will NOT be able to connect! - No comment provided by engineer. - - - The created archive is available via app Settings / Database / Old database archive. - No comment provided by engineer. - - - The group is fully decentralized – it is visible only to the members. - No comment provided by engineer. - - - The microphone does not work when the app is in the background. - No comment provided by engineer. - - - The next generation of private messaging - No comment provided by engineer. - - - The old database was not removed during the migration, it can be deleted. - No comment provided by engineer. - - - The profile is only shared with your contacts. - No comment provided by engineer. - - - The sender will NOT be notified - No comment provided by engineer. - - - The servers for new connections of your current chat profile **%@**. - No comment provided by engineer. - - - Theme - No comment provided by engineer. - - - This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - No comment provided by engineer. - - - This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - No comment provided by engineer. - - - This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - No comment provided by engineer. - - - 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). - No comment provided by engineer. - - - This group no longer exists. - No comment provided by engineer. - - - This setting applies to messages in your current chat profile **%@**. - No comment provided by engineer. - - - To ask any questions and to receive updates: - No comment provided by engineer. - - - To find the profile used for an incognito connection, tap the contact or group name on top of the chat. - No comment provided by engineer. - - - To make a new connection - No comment provided by engineer. - - - To prevent the call interruption, enable Do Not Disturb mode. - 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. - No comment provided by engineer. - - - To protect your information, turn on SimpleX Lock. -You will be prompted to complete authentication before this feature is enabled. - No comment provided by engineer. - - - To record voice message please grant permission to use Microphone. - No comment provided by engineer. - - - To support instant push notifications the chat database has to be migrated. - No comment provided by engineer. - - - To verify end-to-end encryption with your contact compare (or scan) the code on your devices. - No comment provided by engineer. - - - Transfer images faster - No comment provided by engineer. - - - Transport isolation - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact (error: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - No comment provided by engineer. - - - Turn off - No comment provided by engineer. - - - Turn off notifications? - No comment provided by engineer. - - - Turn on - No comment provided by engineer. - - - Unable to record voice message - No comment provided by engineer. - - - Unexpected error: %@ - No comment provided by engineer. - - - Unexpected migration state - No comment provided by engineer. - - - Unknown database error: %@ - No comment provided by engineer. - - - Unknown error - 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. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - No comment provided by engineer. - - - Unlock - authentication reason - - - Unmute - No comment provided by engineer. - - - Unread - No comment provided by engineer. - - - Update - No comment provided by engineer. - - - Update .onion hosts setting? - 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? - 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. - - - Use .onion hosts - No comment provided by engineer. - - - Use SimpleX Chat servers? - No comment provided by engineer. - - - Use chat - No comment provided by engineer. - - - Use for new connections - No comment provided by engineer. - - - Use server - No comment provided by engineer. - - - User profile - No comment provided by engineer. - - - Using .onion hosts requires compatible VPN provider. - No comment provided by engineer. - - - Using SimpleX Chat servers. - No comment provided by engineer. - - - Verify connection security - No comment provided by engineer. - - - Verify security code - No comment provided by engineer. - - - Via browser - No comment provided by engineer. - - - Video call - No comment provided by engineer. - - - View security code - No comment provided by engineer. - - - Voice messages - chat feature - - - Voice messages are prohibited in this chat. - No comment provided by engineer. - - - Voice messages are prohibited in this group. - No comment provided by engineer. - - - Voice messages prohibited! - No comment provided by engineer. - - - Voice message… - No comment provided by engineer. - - - Waiting for file - No comment provided by engineer. - - - Waiting for image - No comment provided by engineer. - - - WebRTC ICE servers - No comment provided by engineer. - - - Welcome %@! - No comment provided by engineer. - - - Welcome message - No comment provided by engineer. - - - What's new - No comment provided by engineer. - - - When available - No comment provided by engineer. - - - When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. - No comment provided by engineer. - - - With optional welcome message. - No comment provided by engineer. - - - Wrong database passphrase - No comment provided by engineer. - - - Wrong passphrase! - No comment provided by engineer. - - - You - No comment provided by engineer. - - - You accepted connection - No comment provided by engineer. - - - You allow - No comment provided by engineer. - - - You are already connected to %@. - No comment provided by engineer. - - - You are connected to the server used to receive messages from this contact. - No comment provided by engineer. - - - You are invited to group - No comment provided by engineer. - - - You 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 now send messages to %@ - notification body - - - You can set lock screen notification preview via settings. - No comment provided by engineer. - - - You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. - No comment provided by engineer. - - - You can share 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. - No comment provided by engineer. - - - You can start chat via app Settings / Database or by restarting the app - No comment provided by engineer. - - - You can use markdown to format messages: - No comment provided by engineer. - - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - - - You could not be verified; please try again. - No comment provided by engineer. - - - You have no chats - No comment provided by engineer. - - - You have to enter passphrase every time the app starts - it is not stored on the device. - No comment provided by engineer. - - - You invited your contact - No comment provided by engineer. - - - You joined this group - No comment provided by engineer. - - - You joined this group. Connecting to inviting group member. - No comment provided by engineer. - - - You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. - No comment provided by engineer. - - - You need to allow your contact to send voice messages to be able to send them. - No comment provided by engineer. - - - You rejected group invitation - No comment provided by engineer. - - - You sent group invitation - No comment provided by engineer. - - - You will be connected to group when the group host's device is online, please wait or check later! - No comment provided by engineer. - - - You will be connected when your connection request is accepted, please wait or check later! - No comment provided by engineer. - - - You will be connected when your contact's device is online, please wait or check later! - No comment provided by engineer. - - - You will be required to authenticate when you start or resume the app after 30 seconds in background. - No comment provided by engineer. - - - You will join a group this link refers to and connect to its group members. - No comment provided by engineer. - - - You will stop receiving messages from this group. Chat history will be preserved. - No comment provided by engineer. - - - You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile - No comment provided by engineer. - - - You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed - No comment provided by engineer. - - - Your ICE servers - No comment provided by engineer. - - - Your SMP servers - No comment provided by engineer. - - - Your SimpleX contact address - No comment provided by engineer. - - - Your calls - No comment provided by engineer. - - - Your chat database - No comment provided by engineer. - - - Your chat database is not encrypted - set passphrase to encrypt it. - No comment provided by engineer. - - - Your chat profile - No comment provided by engineer. - - - Your chat profile will be sent to group members - No comment provided by engineer. - - - Your chat profile will be sent to your contact - No comment provided by engineer. - - - Your chat profiles - No comment provided by engineer. - - - Your chat profiles are stored locally, only on your device. - No comment provided by engineer. - - - Your chats - No comment provided by engineer. - - - Your contact address - No comment provided by engineer. - - - Your contact can scan it from the app. - No comment provided by engineer. - - - 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). - 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 current chat database will be DELETED and REPLACED with the imported one. - No comment provided by engineer. - - - Your current profile - No comment provided by engineer. - - - Your preferences - No comment provided by engineer. - - - Your privacy - No comment provided by engineer. - - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - No comment provided by engineer. - - - Your profile will be sent to the contact that you received this link from - No comment provided by engineer. - - - Your profile, contacts and delivered messages are stored on your device. - No comment provided by engineer. - - - Your random profile - No comment provided by engineer. - - - Your server - No comment provided by engineer. - - - Your server address - No comment provided by engineer. - - - Your settings - No comment provided by engineer. - - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - - - [Send us email](mailto:chat@simplex.chat) - No comment provided by engineer. - - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - - - \_italic_ - No comment provided by engineer. - - - \`a + b` - No comment provided by engineer. - - - above, then choose: - No comment provided by engineer. - - - accepted call - call status - - - admin - member role - - - always - pref value - - - audio call (not e2e encrypted) - No comment provided by engineer. - - - bad message ID - integrity error chat item - - - bad message hash - integrity error chat item - - - bold - No comment provided by engineer. - - - call error - call status - - - call in progress - call status - - - calling… - call status - - - cancelled %@ - feature offered item - - - changed address for you - chat item text - - - changed role of %1$@ to %2$@ - rcv group event chat item - - - changed your role to %@ - rcv group event chat item - - - changing address for %@... - chat item text - - - changing address... - chat item text - - - colored - No comment provided by engineer. - - - complete - No comment provided by engineer. - - - connect to SimpleX Chat developers. - No comment provided by engineer. - - - connected - No comment provided by engineer. - - - connecting - No comment provided by engineer. - - - connecting (accepted) - No comment provided by engineer. - - - connecting (announced) - No comment provided by engineer. - - - connecting (introduced) - No comment provided by engineer. - - - connecting (introduction invitation) - No comment provided by engineer. - - - connecting call… - call status - - - connecting… - chat list item title - - - connection established - chat list item title (it should not be shown - - - connection:%@ - connection information - - - contact has e2e encryption - No comment provided by engineer. - - - contact has no e2e encryption - No comment provided by engineer. - - - creator - No comment provided by engineer. - - - default (%@) - pref value - - - deleted - deleted chat item - - - deleted group - rcv group event chat item - - - direct - connection level description - - - duplicate message - integrity error chat item - - - e2e encrypted - No comment provided by engineer. - - - enabled - enabled status - - - enabled for contact - enabled status - - - enabled for you - enabled status - - - ended - No comment provided by engineer. - - - ended call %@ - call status - - - error - No comment provided by engineer. - - - group deleted - No comment provided by engineer. - - - group profile updated - snd group event chat item - - - iOS Keychain is used to securely store passphrase - it allows receiving push notifications. - 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. - No comment provided by engineer. - - - incognito via contact address link - chat list item description - - - incognito via group link - chat list item description - - - incognito via one-time link - chat list item description - - - indirect (%d) - connection level description - - - invalid chat - invalid chat data - - - invalid chat data - No comment provided by engineer. - - - invalid data - invalid chat item - - - invitation to group %@ - group name - - - invited - No comment provided by engineer. - - - invited %@ - rcv group event chat item - - - invited to connect - chat list item title - - - invited via your group link - rcv group event chat item - - - italic - No comment provided by engineer. - - - join as %@ - No comment provided by engineer. - - - left - rcv group event chat item - - - marked deleted - marked deleted chat item preview text - - - member - member role - - - connected - rcv group event chat item - - - message received - notification - - - missed call - call status - - - never - No comment provided by engineer. - - - new message - notification - - - no - pref value - - - no e2e encryption - No comment provided by engineer. - - - off - enabled status - group pref value - - - offered %@ - feature offered item - - - offered %1$@: %2$@ - feature offered item - - - on - group pref value - - - or chat with the developers - No comment provided by engineer. - - - owner - member role - - - peer-to-peer - No comment provided by engineer. - - - received answer… - No comment provided by engineer. - - - received confirmation… - No comment provided by engineer. - - - rejected call - call status - - - removed - No comment provided by engineer. - - - removed %@ - rcv group event chat item - - - removed you - rcv group event chat item - - - sec - network option - - - secret - No comment provided by engineer. - - - starting… - No comment provided by engineer. - - - strike - No comment provided by engineer. - - - this contact - notification title - - - unknown - connection info - - - updated group profile - rcv group event chat item - - - v%@ (%@) - No comment provided by engineer. - - - via contact address link - chat list item description - - - via group link - chat list item description - - - via one-time link - chat list item description - - - via relay - No comment provided by engineer. - - - video call (not e2e encrypted) - No comment provided by engineer. - - - waiting for answer… - No comment provided by engineer. - - - waiting for confirmation… - No comment provided by engineer. - - - wants to connect to you! - No comment provided by engineer. - - - yes - pref value - - - you are invited to group - No comment provided by engineer. - - - you changed address - chat item text - - - you changed address for %@ - chat item text - - - you changed role for yourself to %@ - snd group event chat item - - - you changed role of %1$@ to %2$@ - snd group event chat item - - - you left - snd group event chat item - - - you removed %@ - snd group event chat item - - - you shared one-time link - chat list item description - - - you shared one-time link incognito - chat list item description - - - you: - No comment provided by engineer. - - - \~strike~ - No comment provided by engineer. - - -
- -
- -
- - - SimpleX - Bundle name - - - SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - Privacy - Camera Usage Description - - - SimpleX uses Face ID for local authentication - Privacy - Face ID Usage Description - - - SimpleX needs microphone access for audio and video calls, and to record voice messages. - Privacy - Microphone Usage Description - - - SimpleX needs access to Photo Library for saving captured and received media - Privacy - Photo Library Additions Usage Description - - -
- -
- -
- - - SimpleX NSE - Bundle display name - - - SimpleX NSE - Bundle name - - - Copyright © 2022 SimpleX Chat. All rights reserved. - Copyright (human-readable) - - -
-
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 @@
%lld - + No comment provided by engineer. %lld %@ - + No comment provided by engineer. @@ -144,12 +144,12 @@ %lldd - + No comment provided by engineer. %lldh - + No comment provided by engineer. @@ -158,7 +158,7 @@ %lldm - + No comment provided by engineer. @@ -173,17 +173,14 @@ %lldw No comment provided by engineer. - + ( + ( No comment provided by engineer. - + ) - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Dodajte novi kontakt**: da biste stvorili svoj jednokratni QR kôd ili vezu za svoj kontakt. + ) No comment provided by engineer. @@ -191,13 +188,13 @@ **Stvorite vezu / QR kôd** za vaš kontakt. 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. + + **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. **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. 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). + + **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. **Najprivatniji**: nemojte koristiti SimpleX Chat poslužitelj obavijesti, povremeno provjeravajte poruke u pozadini (ovisi o tome koliko često koristite aplikaciju). No comment provided by engineer. @@ -211,8 +208,8 @@ **Imajte na umu**: NEĆETE moći oporaviti ili promijeniti pristupni izraz ako ga izgubite. 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. + + **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. **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. No comment provided by engineer. @@ -253,22 +250,22 @@ 1 day - 1 dan + 1 dan message ttl 1 hour - 1 sat + 1 sat message ttl 1 month - 1 mjesec + 1 mesec message ttl 1 week - 1 tjedan + 1 nedelja message ttl @@ -367,8 +364,8 @@ Add servers by scanning QR codes. No comment provided by engineer. - - Add server… + + Add server No comment provided by engineer. @@ -1039,8 +1036,8 @@ Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1055,8 +1052,8 @@ 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. @@ -1419,16 +1416,16 @@ Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1519,20 +1516,23 @@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. - + Import + Uvesti No comment provided by engineer. - + Import chat database? + Uvesti data bazu razgovora? No comment provided by engineer. - + Import database + Uvesti data bazu No comment provided by engineer. @@ -1616,8 +1616,8 @@ 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. @@ -1693,12 +1693,14 @@ We will be adding server redundancy to prevent lost messages. Live message! No comment provided by engineer. - + Live messages + Žive poruke No comment provided by engineer. - + Local name + Lokalno ime No comment provided by engineer. @@ -1917,8 +1919,8 @@ We will be adding server redundancy to prevent lost messages. 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**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -1965,8 +1967,8 @@ We will be adding server redundancy to prevent lost messages. Open chat console authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -1997,8 +1999,8 @@ We will be adding server redundancy to prevent lost messages. Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2577,8 +2579,8 @@ We will be adding server redundancy to prevent lost messages. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -2609,16 +2611,16 @@ We will be adding server redundancy to prevent lost messages. The microphone does not work when the app is in the background. No comment provided by engineer. - - The next generation of private messaging + + The future of 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. @@ -2673,8 +2675,8 @@ We will be adding server redundancy to prevent lost messages. To prevent the call interruption, enable Do Not Disturb mode. 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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -2847,8 +2849,8 @@ To connect, please ask your contact to create another connection link and check 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. @@ -2959,10 +2961,6 @@ To connect, please ask your contact to create another connection link and check You can use markdown to format messages: 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. @@ -3161,8 +3159,9 @@ SimpleX servers cannot see your profile. \_italic_ No comment provided by engineer. - + \`a + b` + \`a + b` No comment provided by engineer. @@ -3622,6 +3621,110 @@ SimpleX servers cannot see your profile. \~strike~ No comment provided by engineer. + + # %@ + # %@ + + + %@ server + %@ server + + + %@ servers + %@ serveri + + + Import failed + Uvoz neuspešan + + + %@ downloaded + %@ preuzeto + + + %@ uploaded + %@ otpremljeno + + + 1 minute + 1 minut + + + Password + Šifra + + + ## History + ## Istorija + + + %@ (current) + %@ (trenutan) + + + %@ and %@ + %@ i %@ + + + %@ connected + %@ povezan + + + 0 sec + 0 sek + + + 5 minutes + 5 minuta + + + %@ (current): + %@ (trenutan): + + + %@ and %@ connected + %@ i %@ su povezani + + + %@: + %@: + + + %1$@ at %2$@: + %1$@ u %2$@: + + + 30 seconds + 30 sekundi + + + Password to show + Prikazati šifru + + + %1$@, %2$@ + %1$@, %2$@ + + + 0s + 0s + + + Import theme + Uvesti temu + + + Immediately + Odmah + + + Address settings + Podešavanje adrese + + + Admins can block a member for all. + Administratori mogu da blokiraju + diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index ad3148d891..78bee138e4 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -2,36 +2,9 @@
- +
- - - - - - 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) (másolható) @@ -54,12 +27,12 @@ ## In reply to - ## Válaszul erre: + ## Válaszul erre copied message info #secret# - #titkos# + #titok# No comment provided by engineer. @@ -99,7 +72,7 @@ %1$@ at %2$@: - %1$@ ekkor: %2$@ + %1$@ ekkor: %2$@: copied message info, <sender> at <time> @@ -119,12 +92,17 @@ %@ is not verified - %@ nem ellenőrzött + %@ nincs hitelesítve No comment provided by engineer. %@ is verified - %@ ellenőrizve + %@ hitelesítve + No comment provided by engineer. + + + %@ server + %@ kiszolgáló No comment provided by engineer. @@ -142,6 +120,11 @@ %@ kapcsolódni szeretne! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ és további %lld tag @@ -162,11 +145,36 @@ %d nap time interval + + %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. + forward confirmation reason + %d hours %d óra time interval + + %d messages not forwarded + %d üzenet nem lett továbbítva + alert title + %d min %d perc @@ -182,6 +190,11 @@ %d mp time interval + + %d seconds(s) + %d másodperc + delete after time + %d skipped message(s) %d üzenet kihagyva @@ -204,12 +217,12 @@ %lld contact(s) selected - %lld ismerős kiválasztva + %lld partner kijelölve No comment provided by engineer. %lld file(s) with total size of %@ - %lld fájl, amely(ek)nek teljes mérete: %@ + %lld fájl, %@ összméretben No comment provided by engineer. @@ -224,22 +237,22 @@ %lld messages blocked - %lld üzenet blokkolva + %lld üzenet letiltva No comment provided by engineer. %lld messages blocked by admin - %lld üzenet blokkolva az admin által + %lld üzenetet letiltott az adminisztrátor No comment provided by engineer. %lld messages marked deleted - %lld törlésre megjelölt üzenet + %lld üzenet megjelölve törlésre No comment provided by engineer. %lld messages moderated by %@ - %lld üzenet moderálva lett %@ által + %@ %lld üzenetet moderált No comment provided by engineer. @@ -249,27 +262,22 @@ %lld new interface languages - %lld új nyelvi csomag - No comment provided by engineer. - - - %lld second(s) - %lld másodperc + %lld új kezelőfelületi nyelv No comment provided by engineer. %lld seconds - %lld másodperc + %lld mp No comment provided by engineer. %lldd - %lldd + %lldn No comment provided by engineer. %lldh - %lldh + %lldó No comment provided by engineer. @@ -279,27 +287,27 @@ %lldm - %lldm + %lldp No comment provided by engineer. %lldmth - %lldmth + %lldh No comment provided by engineer. %llds - %llds + %lldmp No comment provided by engineer. %lldw - %lldw + %lldhét No comment provided by engineer. %u messages failed to decrypt. - %u üzenet visszafejtése sikertelen. + Nem sikerült visszafejteni %u üzenetet. No comment provided by engineer. @@ -307,11 +315,6 @@ %u üzenet kihagyva. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (új) @@ -322,59 +325,54 @@ (ez az eszköz: v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása a kapcsolattartóhoz. + + **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. + **Csoport létrehozása:** új csoport létrehozásához. 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. - **Privátabb**: 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken megosztásra kerül a SimpleX Chat kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van. + + **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 notifications server, check messages periodically in the background (depends on how often you use the app). - **Legprivátabb**: ne használja a SimpleX Chat értesítési szervert, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. + **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 kapcsolataiból érkező üzenetek visszafejtését. + **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. - **Figyelem**: NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt. + **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 notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. - **Javasolt**: az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési szerverre, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik. + + **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 kulcstárolóban tárolt jelmondat megadása szükséges. + **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. - **Figyelem**: az archívum törlésre kerül. + **Figyelmeztetés:** az archívum el lesz távolítva. No comment provided by engineer. @@ -392,11 +390,6 @@ \*félkövér* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -411,7 +404,7 @@ - a bit better groups. - and more! - stabilabb üzenetkézbesítés. -- valamivel jobb csoportok. +- picit továbbfejlesztett csoportok. - és még sok más! No comment provided by engineer. @@ -419,8 +412,8 @@ - optionally notify deleted contacts. - profile names with spaces. - and more! - - opcionális értesítés a törölt kapcsolatokról. -- profilnevek szóközökkel. + - partnerek értesítése a törlésről (nem kötelező) +- profilnevek szóközökkel - és még sok más! No comment provided by engineer. @@ -428,16 +421,11 @@ - voice messages up to 5 minutes. - custom time to disappear. - editing history. - - hangüzenetek legfeljebb 5 perces időtartamig. -- egyedi eltűnési időhatár megadása. + - legfeljebb 5 perc hosszúságú hangüzenetek. +- egyéni üzenet-eltűnési időkorlát. - előzmények szerkesztése. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 mp @@ -451,7 +439,8 @@ 1 day 1 nap - time interval + delete after time +time interval 1 hour @@ -466,12 +455,29 @@ 1 month 1 hónap - time interval + delete after time +time interval 1 week 1 hét - time interval + delete after time +time interval + + + 1 year + 1 év + delete after time + + + 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 @@ -488,43 +494,38 @@ 30 másodperc No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> <p>Üdvözlöm!</p> -<p><a href="%@">Csatlakozzon hozzám a SimpleX Chaten</a></p> +<p><a href="%@">Csatlakozzon hozzám a SimpleX Chaten keresztül</a></p> email text A few more things - Még néhány dolog + Néhány további dolog No comment provided by engineer. A new contact - Egy új ismerős + Egy új partner notification title A new random profile will be shared. - Egy új, véletlenszerű profil kerül megosztásra. + 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**. - A rendszer külön TCP-kapcsolatot fog használni **az alkalmazásban található minden csevegési profilhoz**. + **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**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail. - A rendszer külön TCP-kapcsolatot fog használni **minden ismerőshöz és csoporttaghoz**. -**Figyelem**: sok kapcsolódás esetén, az akkumulátor- és adatforgalom fogyasztás jelentősen megnőhet, és egyes kapcsolatok meghiúsulhatnak. + **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. No comment provided by engineer. @@ -534,17 +535,12 @@ Abort changing address - Címváltoztatás megszakítása + Cím módosításának megszakítása No comment provided by engineer. Abort changing address? - Címváltoztatás megszakítása?? - No comment provided by engineer. - - - About SimpleX - A SimpleX névjegye + Megszakítja a cím módosítását? No comment provided by engineer. @@ -552,50 +548,82 @@ A SimpleX Chat névjegye No comment provided by engineer. - - About SimpleX address - A SimpleX azonosítóról + + About operators + Az üzemeltetőkről No comment provided by engineer. - - Accent color - Kiemelő szín + + Accent + Kiemelőszín No comment provided by engineer. Accept Elfogadás accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Feltételek elfogadása + No comment provided by engineer. Accept connection request? - Kapcsolatfelvétel elfogadása? + Elfogadja a meghívási kérést? No comment provided by engineer. Accept contact request from %@? - Elfogadja %@ kapcsolat kérését? + Elfogadja %@ meghívási kérését? notification body Accept incognito - Fogadás inkognítóban - accept contact request via notification + Elfogadás inkognitóban + accept contact request via notification +swipe action + + + Accepted conditions + Elfogadott feltételek + No comment provided by engineer. + + + Acknowledged + Visszaigazolt + No comment provided by engineer. + + + Acknowledgement errors + Visszaigazolási hibák + No comment provided by engineer. + + + Active + Aktív + token status text + + + 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. - Azonosító hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára. + 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 contact - Ismerős hozzáadása + + Add friends + Barátok hozzáadása No comment provided by engineer. - - Add preset servers - Előre beállított kiszolgálók hozzáadása + + Add list + Lista hozzáadása No comment provided by engineer. @@ -603,14 +631,19 @@ 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 server… - Kiszolgáló hozzáadása… + + Add team members + Munkatársak hozzáadása No comment provided by engineer. @@ -618,9 +651,44 @@ 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 + Ü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. @@ -630,17 +698,27 @@ Address change will be aborted. Old receiving address will be used. - A cím módosítása megszakad. A régi fogadási cím kerül felhasználásra. + 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 No comment provided by engineer. Admins can block a member for all. - Az adminok egy tagot mindenki számára letilthatnak. + 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 adminok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz. + Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. No comment provided by engineer. @@ -648,54 +726,94 @@ Speciális hálózati beállítások No comment provided by engineer. + + Advanced settings + Speciális beállítások + No comment provided by engineer. + + + All + Összes + No comment provided by engineer. + All app data is deleted. - Minden alkalmazásadat törölve. + Az összes alkalmazásadat törölve. No comment provided by engineer. All chats and messages will be deleted - this cannot be undone! - Minden csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza! + Az összes csevegés és üzenet törölve lesz – ez a művelet nem vonható vissza! No comment provided by engineer. + + 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. + alert message + All data is erased when it is entered. - A jelkód megadása után minden adat törlésre kerül. + 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. - Minden csoporttag kapcsolódva marad. + Az összes csoporttag kapcsolatban marad. + 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! - Minden üzenet törlésre kerül – ez a művelet nem vonható vissza! + 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. - Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az ön számára törlődnek. + 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! - Minden új üzenet elrejtésre kerül tőle: %@! + %@ összes új üzenete el lesz rejtve! + No comment provided by engineer. + + + All profiles + Összes profil + profile dropdown + + + 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. - Minden ismerős kapcsolódva marad. + 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. - Ismerőseivel kapcsolatban marad. A profil változtatások frissítésre kerülnek az ismerősöknél. + 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. - Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP átjátszókra. + Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra. No comment provided by engineer. @@ -705,92 +823,112 @@ Allow calls only if your contact allows them. - Hívások engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. + 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. - Eltűnő üzenetek engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi az ön számára. + 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) - Üzenet végleges törlésének engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. (24 óra) + 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. - Üzenetreakciók engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. + 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. - Üzenetreakciók engedélyezése. + A reakciók hozzáadása az üzenetekhez engedélyezve van. No comment provided by engineer. Allow sending direct messages to members. - Közvetlen üzenetek küldésének engedélyezése a tagok számára. + A közvetlen üzenetek küldése a tagok között engedélyezve van. No comment provided by engineer. Allow sending disappearing messages. - Eltűnő üzenetek küldésének engedélyezése. + 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) - Elküldött üzenetek végleges törlésének engedélyezése. (24 óra) + 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 SimpleX links. - A SimpleX hivatkozások küldése engedélyezve van. + A SimpleX-hivatkozások küldése engedélyezve van. No comment provided by engineer. Allow to send files and media. - Fájlok és médiatartalom küldésének engedélyezése. + A fájlok- és a médiatartalmak küldése engedélyezve van. No comment provided by engineer. Allow to send voice messages. - Hangüzenetek küldésének engedélyezése. + A hangüzenetek küldése engedélyezve van. No comment provided by engineer. Allow voice messages only if your contact allows them. - Hangüzenetek küldésének engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. + 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? - Hangüzenetek engedélyezése? + Engedélyezi a hangüzeneteket? No comment provided by engineer. Allow your contacts adding message reactions. - Ismerősök általi üzenetreakciók hozzáadásának engedélyezése. + 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. - Hívások engedélyezése ismerősök számára. + 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) - Elküldött üzenetek végleges törlésének engedélyezése az ismerősök számára. (24 óra) + 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. - Eltűnő üzenetek engedélyezése ismerősök számára. + 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. - Hangüzenetek küldésének engedélyezése ismerősök számára. + A hangüzenetek küldése engedélyezve van a partnerei számára. No comment provided by engineer. @@ -808,24 +946,39 @@ A csatlakozás folyamatban van a csoporthoz! 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 átjátszó kiszolgálót + 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 jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik. + 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. + + Another reason + Egyéb indoklás + report reason + 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 build száma: %@ + Az alkalmazás összeállítási száma: %@ No comment provided by engineer. @@ -838,9 +991,14 @@ 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ás ikon + Alkalmazásikon No comment provided by engineer. @@ -850,17 +1008,22 @@ App passcode is replaced with self-destruct passcode. - Az alkalmazás jelkód helyettesítésre kerül egy önmegsemmisítő jelkóddal. + 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 - Alkalmazás verzió + Az alkalmazás verziója No comment provided by engineer. App version: v%@ - Alkalmazás verzió: v%@ + Az alkalmazás verziója: v%@ No comment provided by engineer. @@ -873,11 +1036,56 @@ 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? + No comment provided by engineer. + + + Archive reports + Jelentések archiválása + swipe action + + + Archived contacts + Archivált partnerek + No comment provided by engineer. + Archiving database Adatbázis archiválása @@ -885,7 +1093,7 @@ Attach - Csatolás + Mellékelés No comment provided by engineer. @@ -900,17 +1108,17 @@ Audio/video calls - Hang-/videóhívások + Hang- és videóhívások chat feature Audio/video calls are prohibited. - A hang- és videóhívások le vannak tiltva. + A hívások kezdeményezése le van tiltva ebben a csevegésben. No comment provided by engineer. Authentication cancelled - Hitelesítés megszakítva + Hitelesítés visszavonva PIN entry @@ -935,22 +1143,32 @@ Auto-accept contact requests - Ismerős jelölések automatikus elfogadása + Meghívási kérések automatikus elfogadása No comment provided by engineer. Auto-accept images - Fotók automatikus elfogadása + Képek automatikus elfogadása No comment provided by engineer. + + Auto-accept settings + Beállítások automatikus elfogadása + alert title + Back Vissza No comment provided by engineer. + + Background + Háttér + No comment provided by engineer. + Bad desktop address - Hibás számítógép azonosító + Érvénytelen számítógépcím No comment provided by engineer. @@ -960,67 +1178,122 @@ Bad message hash - Téves üzenet hash + Érvénytelen az üzenet hasítóértéke + No comment provided by engineer. + + + Better calls + Továbbfejlesztett hívásélmény No comment provided by engineer. Better groups - Javított csoportok + 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 - Jobb üzenetek + 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 - Blokkolás + Letiltás No comment provided by engineer. Block for all - Mindenki számára letiltva + Letiltás No comment provided by engineer. Block group members - Csoporttagok blokkolása + Csoporttagok letiltása No comment provided by engineer. Block member - Tag blokkolása + Letiltás No comment provided by engineer. Block member for all? - Tag letiltása mindenki számára? + Az összes tag számára letiltja a tagot? No comment provided by engineer. Block member? - Tag blokkolása? + Letiltja a tagot? No comment provided by engineer. Blocked by admin - Letiltva az admin által + 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. Both you and your contact can add message reactions. - Mindkét fél is hozzáadhat üzenetreakciókat. + 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 törölheti véglegesen az elküldött üzeneteket. (24 óra) + 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 indítani. + Mindkét fél tud hívásokat kezdeményezni. No comment provided by engineer. @@ -1038,9 +1311,33 @@ 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). - Csevegési profil (alapértelmezett) vagy [kapcsolat alapján] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA). + A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA). + No comment provided by engineer. + + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + A SimpleX Chat használatával Ön elfogadja, hogy: +- csak elfogadott tartalmakat tesz közzé a nyilvános csoportokban. +- tiszteletben tartja a többi felhasználót, és nem küld kéretlen tartalmat senkinek. No comment provided by engineer. @@ -1053,25 +1350,46 @@ 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 fényképező nem elérhető + 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! - Ismerősök meghívása le van tiltva! + Nem lehet meghívni a partnert! No comment provided by engineer. Can't invite contacts! - Ismerősök meghívása nem lehetséges! + Nem lehet meghívni a partnereket! + No comment provided by engineer. + + + Can't message member + Nem lehet üzenetet küldeni a tagnak No comment provided by engineer. Cancel Mégse - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1080,13 +1398,23 @@ Cannot access keychain to save database password - Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez + 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 No comment provided by engineer. Cannot receive file Nem lehet fogadni a fájlt - No comment provided by engineer. + alert title + + + 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. + snd error text Cellular @@ -1095,58 +1423,83 @@ Change - Változtatás + Módosítás No comment provided by engineer. + + Change automatic message deletion? + Módosítja az automatikus üzenettörlést? + alert title + + + Change chat profiles + Csevegési profilok módosítása + authentication reason + Change database passphrase? - Adatbázis jelmondat megváltoztatása? + Módosítja az adatbázis jelmondatát? No comment provided by engineer. Change lock mode - Zárolási mód megváltoztatása + Zárolási mód módosítása authentication reason Change member role? - Tag szerepkörének megváltoztatása? + Módosítja a tag szerepkörét? No comment provided by engineer. Change passcode - Jelkód megváltoztatása + Jelkód módosítása authentication reason Change receiving address - A fogadó cím megváltoztatása + Fogadási cím módosítása No comment provided by engineer. Change receiving address? - Megváltoztatja a fogadó címet? + Módosítja a fogadási címet? No comment provided by engineer. Change role - Szerepkör megváltoztatása + Szerepkör módosítása No comment provided by engineer. Change self-destruct mode - Önmegsemmisítő mód megváltoztatása + Önmegsemmisítő-mód módosítása authentication reason Change self-destruct passcode - Önmegsemmisító jelkód megváltoztatása + Önmegsemmisítő-jelkód módosítása authentication reason - set passcode view +set passcode view - - Chat archive - Csevegési archívum + + 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. @@ -1164,6 +1517,11 @@ 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 @@ -1176,12 +1534,17 @@ Chat is stopped - A csevegés leállt + 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 leá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 megkezdése előtt. + 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. @@ -1194,15 +1557,50 @@ Csevegési beállítások No comment provided by engineer. + + Chat preferences were changed. + A csevegési beállítások módosultak. + alert message + + + 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. + No comment provided by engineer. + Check server address and try again. Kiszolgáló címének ellenőrzése és újrapróbálkozás. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1211,7 +1609,7 @@ 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 szkennelje be a QR-kódot. + 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. @@ -1224,24 +1622,49 @@ 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 + No comment provided by engineer. + Clear Kiürítés - No comment provided by engineer. + swipe action Clear conversation - Beszélgetés kiürítése + Üzenetek kiürítése No comment provided by engineer. Clear conversation? - Beszélgetés kiürítése? + 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? - Privát jegyzetek törlése? + Kiüríti a privát jegyzeteket? No comment provided by engineer. @@ -1249,24 +1672,79 @@ Hitelesítés törlése No comment provided by engineer. - - Colors - Színek + + 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. + + + Community guidelines violation + Közösségi irányelvek megsértése + report reason + Compare file - Fájl összehasonlítás + Fájl-összehasonlítás server test step Compare security codes with your contacts. - Biztonsági kódok összehasonlítása az ismerősökkel. + Biztonsági kódok összehasonlítása a partnerekével. + 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 + 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. @@ -1279,9 +1757,19 @@ Jelkód megerősítése No comment provided by engineer. + + Confirm contact deletion? + Biztosan törli a partnert? + No comment provided by engineer. + Confirm database upgrades - Adatbázis frissítés megerősítése + 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. @@ -1301,7 +1789,7 @@ Confirm that you remember database passphrase to migrate it. - Erősítse meg, hogy emlékszik az adatbázis jelmondatára az átköltöztetéshez. + Az átköltöztetéshez erősítse meg, hogy emlékszik az adatbázis jelmondatára. No comment provided by engineer. @@ -1309,6 +1797,11 @@ Feltöltés megerősítése No comment provided by engineer. + + Confirmed + Megerősítve + token status text + Connect Kapcsolódás @@ -1326,31 +1819,36 @@ Connect to desktop - Kapcsolódás számítógéphez + Társítás számítógéppel + No comment provided by engineer. + + + Connect to your friends faster. + Kapcsolódjon gyorsabban a partnereihez. No comment provided by engineer. Connect to yourself? - Kapcsolódás saját magához? + Kapcsolódik saját magához? No comment provided by engineer. Connect to yourself? This is your own SimpleX address! - Kapcsolódás saját magához? -Ez a SimpleX azonosítója! + Kapcsolódik saját magához? +Ez a saját SimpleX-címe! No comment provided by engineer. Connect to yourself? This is your own one-time link! - Kapcsolódás saját magához? -Ez az egyszer használatos hivatkozása! + Kapcsolódik saját magához? +Ez a saját egyszer használható meghívója! No comment provided by engineer. Connect via contact address - Kapcsolódás a kapcsolattartási azonosítón keresztül + Kapcsolódás a kapcsolattartási címen keresztül No comment provided by engineer. @@ -1360,17 +1858,27 @@ Ez az egyszer használatos hivatkozása! Connect via one-time link - Kapcsolódás egyszer használatos hivatkozáson keresztül + Kapcsolódás egyszer használható meghívón keresztül No comment provided by engineer. Connect with %@ - Kapcsolódás ezzel: %@ + Kapcsolódás a következővel: %@ + No comment provided by engineer. + + + Connected + Kapcsolódott No comment provided by engineer. Connected desktop - Csatlakoztatott számítógép + Társított számítógép + No comment provided by engineer. + + + Connected servers + Kapcsolódott kiszolgálók No comment provided by engineer. @@ -1378,6 +1886,11 @@ Ez az egyszer használatos hivatkozása! Kapcsolódva a számítógéphez No comment provided by engineer. + + Connecting + Kapcsolódás + No comment provided by engineer. + Connecting to server… Kapcsolódás a kiszolgálóhoz… @@ -1385,7 +1898,12 @@ Ez az egyszer használatos hivatkozása! Connecting to server… (error: %@) - Kapcsolódás a kiszolgálóhoz... (hiba: %@) + 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. @@ -1398,6 +1916,16 @@ Ez az egyszer használatos hivatkozása! 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 @@ -1408,9 +1936,36 @@ Ez az egyszer használatos hivatkozása! Kapcsolódási hiba (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + A kiszolgáló üzemeltetője letiltotta a kapcsolatot: +%@ + 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! - Kapcsolódási kérés elküldve! + 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. @@ -1420,72 +1975,112 @@ Ez az egyszer használatos hivatkozása! Connection timeout - Kapcsolat időtúllépés + 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 + No comment provided by engineer. + + + Connections + Kapcsolatok No comment provided by engineer. Contact allows - Ismerős engedélyezi + Partner engedélyezi No comment provided by engineer. Contact already exists - Létező ismerős + A partner már létezik + No comment provided by engineer. + + + Contact deleted! + Partner törölve! No comment provided by engineer. Contact hidden: - Ismerős elrejtve: + Rejtett név: notification Contact is connected - Ismerőse kapcsolódott + Partnere kapcsolódott notification - - Contact is not connected yet! - Az ismerőse még nem kapcsolódott! + + Contact is deleted. + Törölt partner. No comment provided by engineer. Contact name - Ismerős neve + Csak név No comment provided by engineer. Contact preferences - Ismerős beállításai + 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 - Ismerősök + Partnerek No comment provided by engineer. Contacts can mark messages for deletion; you will be able to view them. - Az ismerősök törlésre jelölhetnek üzeneteket ; megtekintheti őket. + A partnerei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat. No comment provided by engineer. + + Content violates conditions of use + A tartalom sérti a használati feltételeket + blocking reason + 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 - chat item action + No comment provided by engineer. + + + Copy error + Másolási hiba + No comment provided by engineer. Core version: v%@ - Alapverziószám: v%@ + Fő verzió: v%@ + No comment provided by engineer. + + + Corner + Sarok No comment provided by engineer. Correct name to %@? - Név javítása erre: %@? + Helyesbíti a nevet a következőre: %@? No comment provided by engineer. @@ -1493,19 +2088,19 @@ Ez az egyszer használatos hivatkozása! 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 SimpleX address - SimpleX azonosító létrehozása + SimpleX-cím létrehozása No comment provided by engineer. Create a group using a random profile. - Csoport létrehozása véletlenszerűen létrehozott profillal. - No comment provided by engineer. - - - Create an address to let people connect with you. - Azonosító létrehozása, hogy az emberek kapcsolatba léphessenek önnel. + Csoport létrehozása véletlenszerű profillal. No comment provided by engineer. @@ -1520,7 +2115,7 @@ Ez az egyszer használatos hivatkozása! Create group link - Csoportos hivatkozás létrehozása + Csoporthivatkozás létrehozása No comment provided by engineer. @@ -1528,9 +2123,14 @@ Ez az egyszer használatos hivatkozása! 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 az [asztali kliensben](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. @@ -1540,7 +2140,7 @@ Ez az egyszer használatos hivatkozása! Create queue - Várólista létrehozása + Sorba állítás létrehozása server test step @@ -1553,24 +2153,24 @@ Ez az egyszer használatos hivatkozása! Saját profil létrehozása No comment provided by engineer. + + Created + Létrehozva + No comment provided by engineer. + Created at - Létrehozva ekkor: + Létrehozva No comment provided by engineer. Created at: %@ - Létrehozva ekkor: %@ + Létrehozva: %@ copied message info - - Created on %@ - Létrehozva %@ - No comment provided by engineer. - Creating archive link - Archív hivatkozás létrehozása + Archívum hivatkozás létrehozása No comment provided by engineer. @@ -1583,19 +2183,39 @@ Ez az egyszer használatos hivatkozása! Jelenlegi jelkód 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 passphrase… Jelenlegi jelmondat… No comment provided by engineer. + + Current profile + Jelenlegi profil + No comment provided by engineer. + Currently maximum supported file size is %@. - Jelenleg a maximális támogatott fájlméret %@. + Jelenleg támogatott legnagyobb fájl méret: %@. No comment provided by engineer. Custom time - Személyreszabott idő + 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. @@ -1603,24 +2223,29 @@ Ez az egyszer használatos hivatkozása! Sötét No comment provided by engineer. + + Dark mode colors + Sötét mód színei + No comment provided by engineer. + Database ID - Adatbázis ID + Adatbázis-azonosító No comment provided by engineer. Database ID: %d - Adatbázis azonosító: %d + Adatbázis-azonosító: %d copied message info Database IDs and Transport isolation option. - Adatbázis azonosítók és átviteli izolációs beállítások. + Adatbázis-azonosítók és átvitel-izolációs beállítások. No comment provided by engineer. Database downgrade - Visszatérés a korábbi adatbázis verzióra + Adatbázis visszafejlesztése No comment provided by engineer. @@ -1631,50 +2256,50 @@ Ez az egyszer használatos hivatkozása! Database encryption passphrase will be updated and stored in the keychain. - Az adatbázis titkosítási jelmondata frissül és tárolódik a kulcstárolóban. + Az adatbázis titkosítási jelmondata frissülni fog és a kulcstartóban lesz tárolva. No comment provided by engineer. Database encryption passphrase will be updated. - Adatbázis titkosítási jelmondat frissítve lesz. + Az adatbázis titkosítási jelmondata frissítve lesz. No comment provided by engineer. Database error - Adatbázis hiba + Adatbázishiba 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, megváltoztatható. + 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őtti módosítás szükséges. + 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 + Adatbázis-jelmondat No comment provided by engineer. Database passphrase & export - Adatbázis jelmondat és exportálás + 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 eltér a kulcstárlóban mentettől. + Az adatbázis jelmondata nem egyezik a kulcstartóba mentettől. No comment provided by engineer. Database passphrase is required to open chat. - Adatbázis jelmondat szükséges a csevegés megnyitásához. + A csevegés megnyitásához adja meg az adatbázis jelmondatát. No comment provided by engineer. @@ -1685,20 +2310,25 @@ Ez az egyszer használatos hivatkozása! Database will be encrypted and the passphrase stored in the keychain. - Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstárolóban lesz tárolva. + Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstartóban lesz tárolva. No comment provided by engineer. Database will be encrypted. - Az adatbázis titkosításra kerül. + Az adatbázis titkosítva lesz. No comment provided by engineer. Database will be migrated when the app restarts - Az adatbázis az alkalmazás újraindításakor migrálásra kerül + Az adatbázis az alkalmazás újraindításakor lesz átköltöztetve + No comment provided by engineer. + + + Debug delivery + Kézbesítési hibák felderítése No comment provided by engineer. @@ -1714,51 +2344,52 @@ Ez az egyszer használatos hivatkozása! Delete Törlés - chat item action + alert action +swipe action + + + 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 Contact - Ismerős törlése - No comment provided by engineer. - Delete address - Azonosító törlése + Cím törlése No comment provided by engineer. Delete address? - Azonosító törlése? + Törli a címet? No comment provided by engineer. Delete after - Törlés miután + Törlés ennyi idő után No comment provided by engineer. Delete all files - Minden fájl törlése + Az összes fájl törlése No comment provided by engineer. Delete and notify contact - Törlés és ismerős értesítése + Törlés, és a partner értesítése No comment provided by engineer. - - Delete archive - Archívum törlése + + Delete chat + Csevegés törlése No comment provided by engineer. - - Delete chat archive? - Csevegési archívum törlése? + + Delete chat messages from your device. + Csevegési üzenetek törlése a saját eszközéről. No comment provided by engineer. @@ -1768,7 +2399,12 @@ Ez az egyszer használatos hivatkozása! Delete chat profile? - Csevegési profil törlése? + Törli a csevegési profilt? + No comment provided by engineer. + + + Delete chat? + Törli a csevegést? No comment provided by engineer. @@ -1778,14 +2414,12 @@ Ez az egyszer használatos hivatkozása! Delete contact - Ismerős törlése + Partner törlése No comment provided by engineer. - - Delete contact? -This cannot be undone! - Ismerős törlése? -Ez a művelet nem vonható vissza! + + Delete contact? + Törli a partnert? No comment provided by engineer. @@ -1805,22 +2439,22 @@ Ez a művelet nem vonható vissza! Delete files and media? - Fájlok és a médiatartalmak törlése? + 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 minden csevegési profilból + Fájlok törlése az összes csevegési profilból No comment provided by engineer. Delete for everyone - Törlés mindenkinél + Törlés az összes tagnál chat feature Delete for me - Törlés nálam + Csak nálam No comment provided by engineer. @@ -1830,7 +2464,7 @@ Ez a művelet nem vonható vissza! Delete group? - Csoport törlése? + Törli a csoportot? No comment provided by engineer. @@ -1840,32 +2474,37 @@ Ez a művelet nem vonható vissza! Delete link - Hivatkozás törlése + Törlés No comment provided by engineer. Delete link? - Hivatkozás törlése? + Törli a hivatkozást? No comment provided by engineer. + + Delete list? + Törli a listát? + alert title + Delete member message? - Csoporttag üzenet törlése? + Törli a tag üzenetét? No comment provided by engineer. Delete message? - Üzenet törlése? + Törli az üzenetet? No comment provided by engineer. Delete messages Üzenetek törlése - No comment provided by engineer. + alert button Delete messages after - Üzenetek törlése miután + Üzenetek törlése ennyi idő után No comment provided by engineer. @@ -1875,17 +2514,17 @@ Ez a művelet nem vonható vissza! Delete old database? - Régi adatbázis törlése? + Törli a régi adatbázist? No comment provided by engineer. - - Delete pending connection - Függőben lévő kapcsolat törlése + + 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? - Függő kapcsolatfelvételi kérések törlése? + Törli a függőben lévő meghívót? No comment provided by engineer. @@ -1895,24 +2534,54 @@ Ez a művelet nem vonható vissza! Delete queue - Várólista törlése + Sorba állítás törlése server test step + + 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? - Felhasználói profil törlése? + Törli a felhasználói profilt? + No comment provided by engineer. + + + Delete without notification + Törlés értesítés nélkül + No comment provided by engineer. + + + Deleted + Törölve No comment provided by engineer. Deleted at - Törölve ekkor: + Törölve No comment provided by engineer. Deleted at: %@ - Törölve ekkor: %@ + Törölve: %@ copied message info + + 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 @@ -1920,12 +2589,12 @@ Ez a művelet nem vonható vissza! Delivery receipts are disabled! - Kézbesítési igazolások kikapcsolva! + A kézbesítési jelentések le vannak tiltva! No comment provided by engineer. Delivery receipts! - Kézbesítési igazolások! + Kézbesítési jelentések! No comment provided by engineer. @@ -1935,12 +2604,12 @@ Ez a művelet nem vonható vissza! Desktop address - Számítógép azonosítója + Számítógép címe No comment provided by engineer. Desktop app version %@ is not compatible with this app. - Az asztali kliens verziója %@ nem kompatibilis ezzel az alkalmazással. + A számítógép-alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással. No comment provided by engineer. @@ -1948,11 +2617,41 @@ Ez a művelet nem vonható vissza! 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. + No comment provided by engineer. + + + Destination server error: %@ + Célkiszolgáló-hiba: %@ + snd error text + + + 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 @@ -1965,17 +2664,17 @@ Ez a művelet nem vonható vissza! Device authentication is disabled. Turning off SimpleX Lock. - Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása. + 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. - Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került. + 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 names, avatars and transport isolation. - Különböző nevek, avatarok és átviteli izoláció. + Különböző nevek, profilképek és átvitel-izoláció. No comment provided by engineer. @@ -1983,9 +2682,14 @@ Ez a művelet nem vonható vissza! Közvetlen üzenetek chat feature - - Direct messages between members are prohibited in this group. - Ebben a csoportban tiltott a tagok közötti közvetlen üzenetek küldése. + + 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. @@ -1995,12 +2699,27 @@ Ez a művelet nem vonható vissza! Disable SimpleX Lock - SimpleX zárolás kikapcsolása + SimpleX-zár kikapcsolása authentication reason + + Disable automatic message deletion? + Letiltja az automatikus üzenettörlést? + alert title + + + Disable delete messages + Üzenetek törlésének letiltása + alert button + Disable for all - Letiltás mindenki számára + Letiltás + No comment provided by engineer. + + + Disabled + Letiltva No comment provided by engineer. @@ -2015,22 +2734,22 @@ Ez a művelet nem vonható vissza! Disappearing messages are prohibited in this chat. - Az eltűnő üzenetek le vannak tiltva ebben a csevegésben. + Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben. No comment provided by engineer. - - Disappearing messages are prohibited in this group. - Az eltűnő üzenetek küldése le van tiltva ebben a csoportban. + + Disappearing messages are prohibited. + Az eltűnő üzenetek küldése le van tiltva. No comment provided by engineer. Disappears at - Eltűnik ekkor: + Eltűnik No comment provided by engineer. Disappears at: %@ - Eltűnik ekkor: %@ + Eltűnik: %@ copied message info @@ -2040,12 +2759,12 @@ Ez a művelet nem vonható vissza! Disconnect desktop? - Számítógép leválasztása? + Leválasztja a számítógépet? No comment provided by engineer. Discover and join groups - Helyi csoportok felfedezése és csatlakozás + Csoportok felfedezése és csatlakozás No comment provided by engineer. @@ -2053,24 +2772,44 @@ Ez a művelet nem vonható vissza! Felfedezés helyi hálózaton keresztül 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 SimpleX for emergency calls. - NE használja a SimpleX-et segélyhívásokhoz. + NE használja a SimpleXet segélyhívásokhoz. + No comment provided by engineer. + + + Do NOT use private routing. + NE használjon privát útválasztást. No comment provided by engineer. Do it later - Későbbre halaszt + Befejezés később No comment provided by engineer. Do not send history to new members. - Az előzmények ne kerüljenek elküldésre az új tagok számára. + Az előzmények ne legyenek elküldve az új tagok számára. + No comment provided by engineer. + + + Do not use credentials with proxy. + Ne használja a hitelesítőadatokat proxyval. + No comment provided by engineer. + + + Documents: + Dokumentumok: No comment provided by engineer. Don't create address - Ne hozzon létre azonosítót + Ne hozzon létre címet No comment provided by engineer. @@ -2078,20 +2817,36 @@ Ez a művelet nem vonható vissza! 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 - Visszatérés a korábbi verzióra és a csevegés megnyitása + Visszafejlesztés és a csevegés megnyitása No comment provided by engineer. Download Letöltés - chat item action + alert button +chat item action + + + Download errors + Letöltési hibák + No comment provided by engineer. Download failed @@ -2103,6 +2858,21 @@ Ez a művelet nem vonható vissza! Fájl letöltése server test step + + Download files + Fájlok letöltése + alert action + + + 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 @@ -2115,7 +2885,7 @@ Ez a művelet nem vonható vissza! Duplicate display name! - Duplikált megjelenítési név! + Duplikált megjelenítendő név! No comment provided by engineer. @@ -2123,6 +2893,11 @@ Ez a művelet nem vonható vissza! Időtartam No comment provided by engineer. + + E2E encrypted notifications. + Végpontok közötti titkosított értesítések. + No comment provided by engineer. + Edit Szerkesztés @@ -2130,7 +2905,7 @@ Ez a művelet nem vonható vissza! Edit group profile - A csoport profiljának szerkesztése + Csoportprofil szerkesztése No comment provided by engineer. @@ -2143,20 +2918,25 @@ Ez a művelet nem vonható vissza! Engedélyezés (felülírások megtartásával) 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 SimpleX Lock - SimpleX zárolás engedélyezése + SimpleX-zár bekapcsolása authentication reason Enable TCP keep-alive - TCP életben tartásának engedélyezése + TCP életben tartása No comment provided by engineer. Enable automatic message deletion? - Automatikus üzenet törlés engedélyezése? - No comment provided by engineer. + Engedélyezi az automatikus üzenettörlést? + alert title Enable camera access @@ -2165,7 +2945,7 @@ Ez a művelet nem vonható vissza! Enable for all - Engedélyezés mindenki számára + Engedélyezés az összes tag számára No comment provided by engineer. @@ -2175,7 +2955,7 @@ Ez a művelet nem vonható vissza! Enable instant notifications? - Azonnali értesítések engedélyezése? + Engedélyezi az azonnali értesítéseket? No comment provided by engineer. @@ -2190,7 +2970,7 @@ Ez a művelet nem vonható vissza! Enable periodic notifications? - Időszakos értesítések engedélyezése? + Engedélyezi az időszakos értesítéseket? No comment provided by engineer. @@ -2200,12 +2980,17 @@ Ez a művelet nem vonható vissza! Enable self-destruct passcode - Önmegsemmisítő jelkód engedélyezése + Önmegsemmisítő-jelkód engedélyezése set passcode view + + Enabled + Engedélyezve + No comment provided by engineer. + Enabled for - Engedélyezve + Számukra engedélyezve No comment provided by engineer. @@ -2215,7 +3000,7 @@ Ez a művelet nem vonható vissza! Encrypt database? - Adatbázis titkosítása? + Titkosítja az adatbázist? No comment provided by engineer. @@ -2225,7 +3010,7 @@ Ez a művelet nem vonható vissza! Encrypt stored files & media - Tárolt fájlok és médiatartalmak titkosítása + A tárolt fájlok- és a médiatartalmak titkosítása No comment provided by engineer. @@ -2240,22 +3025,22 @@ Ez a művelet nem vonható vissza! Encrypted message: app is stopped - Titkosított üzenet: az alkalmazás leállt + Titkosított üzenet: az alkalmazás megállt notification Encrypted message: database error - Titkosított üzenet: adatbázis hiba + Titkosított üzenet: adatbázishiba notification Encrypted message: database migration error - Titkosított üzenet: adatbázis-átköltöztetés hiba + Titkosított üzenet: adatbázis-átköltöztetési hiba notification Encrypted message: keychain error - Titkosított üzenet: kulcstároló hiba + Titkosított üzenet: kulcstartó hiba notification @@ -2270,62 +3055,67 @@ Ez a művelet nem vonható vissza! Encryption re-negotiation error - Titkosítás újraegyeztetési hiba + Hiba történt a titkosítás újraegyeztetésekor message decrypt error item Encryption re-negotiation failed. - Sikertelen titkosítás-újraegyeztetés. + Nem sikerült a titkosítást újraegyeztetni. + No comment provided by engineer. + + + Encryption renegotiation in progress. + A titkosítás újraegyeztetése folyamatban van. No comment provided by engineer. Enter Passcode - Jelkód megadása + Adja meg a jelkódot No comment provided by engineer. Enter correct passphrase. - Helyes jelmondat bevitele. + Adja meg a helyes jelmondatot. No comment provided by engineer. Enter group name… - Csoportnév megadása… + Adja meg a csoport nevét… No comment provided by engineer. Enter passphrase - Jelmondat megadása + Adja meg a jelmondatot No comment provided by engineer. Enter passphrase… - Jelmondat megadása… + Adja meg a jelmondatot… No comment provided by engineer. Enter password above to show! - Jelszó megadása a megjelenítéshez! + Adja meg a jelszót fentebb a megjelenítéshez! No comment provided by engineer. Enter server manually - Kiszolgáló megadása kézzel + Adja meg a kiszolgálót kézzel No comment provided by engineer. Enter this device name… - Eszköznév megadása… + Adja meg ennek az eszköznek a nevét… No comment provided by engineer. Enter welcome message… - Üdvözlő üzenetet megadása… + Adja meg az üdvözlőüzenetet… placeholder Enter welcome message… (optional) - Üdvözlő üzenetet megadása… (opcionális) + Adja meg az üdvözlőüzenetet… (nem kötelező) placeholder @@ -2340,212 +3130,282 @@ Ez a művelet nem vonható vissza! Error aborting address change - Hiba az azonosító megváltoztatásának megszakításakor + Hiba történt a cím módosításának megszakításakor No comment provided by engineer. + + Error accepting conditions + Hiba történt a feltételek elfogadásakor + alert title + Error accepting contact request - Hiba történt a kapcsolatfelvételi kérelem elfogadásakor - No comment provided by engineer. - - - Error accessing database file - Hiba az adatbázisfájl elérésekor + Hiba történt a meghívási kérés elfogadásakor No comment provided by engineer. Error adding member(s) - Hiba a tag(-ok) hozzáadásakor + Hiba történt a tag(ok) hozzáadásakor No comment provided by engineer. + + Error adding server + Hiba történt a kiszolgáló hozzáadásakor + alert title + Error changing address - Hiba az azonosító megváltoztatásakor + 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 a szerepkör megváltoztatásakor + Hiba történt a szerepkör módosításakor No comment provided by engineer. Error changing setting - Hiba a beállítás megváltoztatásakor + 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 az azonosító létrehozásakor + Hiba történt a cím létrehozásakor No comment provided by engineer. Error creating group - Hiba a csoport létrehozásakor + Hiba történt a csoport létrehozásakor No comment provided by engineer. Error creating group link - Hiba a csoport hivatkozásának létrehozásakor + Hiba történt a csoporthivatkozás létrehozásakor No comment provided by engineer. + + Error creating list + Hiba történt a lista létrehozásakor + alert title + Error creating member contact - Hiba az ismerőssel történő kapcsolat létrehozásában + Hiba történt a partnerrel történő kapcsolat létrehozásában No comment provided by engineer. Error creating message - Hiba az üzenet létrehozásakor + Hiba történt az üzenet létrehozásakor No comment provided by engineer. Error creating profile! - Hiba a profil létrehozásakor! + 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 a fájl visszafejtésekor + Hiba történt a fájl visszafejtésekor No comment provided by engineer. Error deleting chat database - Hiba a csevegési adatbázis törlésekor + Hiba történt a csevegési adatbázis törlésekor No comment provided by engineer. Error deleting chat! - Hiba a csevegés törlésekor! + Hiba történt a csevegés törlésekor! No comment provided by engineer. Error deleting connection - Hiba a kapcsolat törlésekor - No comment provided by engineer. - - - Error deleting contact - Hiba az ismerős törlésekor + Hiba történt a kapcsolat törlésekor No comment provided by engineer. Error deleting database - Hiba az adatbázis törlésekor + Hiba történt az adatbázis törlésekor No comment provided by engineer. Error deleting old database - Hiba a régi adatbázis törlésekor + Hiba történt a régi adatbázis törlésekor No comment provided by engineer. Error deleting token - Hiba a token törlésekor + Hiba történt a token törlésekor No comment provided by engineer. Error deleting user profile - Hiba a felhasználói profil törlésekor + Hiba történt a felhasználó-profil törlésekor No comment provided by engineer. Error downloading the archive - Hiba az archívum letöltésekor + Hiba történt az archívum letöltésekor No comment provided by engineer. Error enabling delivery receipts! - Hiba a kézbesítési jelentések engedélyezésekor! + Hiba történt a kézbesítési jelentések engedélyezésekor! No comment provided by engineer. Error enabling notifications - Hiba az értesítések engedélyezésekor + Hiba történt az értesítések engedélyezésekor No comment provided by engineer. Error encrypting database - Hiba az adatbázis titkosításakor + Hiba történt az adatbázis titkosításakor No comment provided by engineer. Error exporting chat database - Hiba a csevegési adatbázis exportálásakor + 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 a csevegési adatbázis importálásakor + Hiba történt a csevegési adatbázis importálásakor No comment provided by engineer. Error joining group - Hiba a csoporthoz való csatlakozáskor + Hiba történt a csoporthoz való csatlakozáskor No comment provided by engineer. - - Error loading %@ servers - Hiba a %@ kiszolgálók betöltésekor + + Error loading servers + Hiba történt a kiszolgálók betöltésekor + alert title + + + 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 a csevegés megnyitásakor + Hiba történt a csevegés megnyitásakor No comment provided by engineer. Error receiving file - Hiba a fájl fogadásakor + Hiba történt a fájl fogadásakor + alert title + + + 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 + No comment provided by engineer. + + + Error registering for notifications + Hiba történt az értesítések regisztrálásakor + alert title + Error removing member - Hiba a tag eltávolításakor + Hiba történt a tag eltávolításakor No comment provided by engineer. - - Error saving %@ servers - Hiba történt a %@ kiszolgálók mentése közben + + Error reordering lists + Hiba történt a listák újrarendezésekor + alert title + + + Error resetting statistics + Hiba történt a statisztikák visszaállításakor No comment provided by engineer. Error saving ICE servers - Hiba az ICE kiszolgálók mentésekor + Hiba történt az ICE-kiszolgálók mentésekor No comment provided by engineer. + + Error saving chat list + Hiba történt a csevegési lista mentésekor + alert title + Error saving group profile - Hiba a csoport profil mentésekor + Hiba történt a csoportprofil mentésekor No comment provided by engineer. Error saving passcode - Hiba a jelkód mentése közben + Hiba történt a jelkód mentésekor No comment provided by engineer. Error saving passphrase to keychain - Hiba a jelmondat kulcstárolóba történő mentésekor + Hiba történt a jelmondat kulcstartóba történő mentésekor No comment provided by engineer. + + Error saving servers + Hiba történt a kiszolgálók mentésekor + alert title + Error saving settings - Hiba a beállítások mentésekor + Hiba történt a beállítások mentésekor when migrating Error saving user password - Hiba a felhasználó jelszavának mentésekor + Hiba történt a felhasználó jelszavának mentésekor No comment provided by engineer. Error scanning code: %@ - Hiba a kód beolvasása közben: %@ + Hiba történt a kód beolvasásakor: %@ No comment provided by engineer. Error sending email - Hiba az e-mail küldésekor + Hiba történt az e-mail elküldésekor No comment provided by engineer. @@ -2555,44 +3415,59 @@ Ez a művelet nem vonható vissza! Error sending message - Hiba az üzenet küldésekor + 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 igazolások beállításakor! + Hiba történt a kézbesítési jelentések beállításakor! No comment provided by engineer. Error starting chat - Hiba a csevegés elindításakor + Hiba történt a csevegés elindításakor No comment provided by engineer. Error stopping chat - Hiba a csevegés megállításakor + 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 No comment provided by engineer. Error switching profile! - Hiba a profil váltásakor! - No comment provided by engineer. + Hiba történt a profilváltáskor! + alertTitle Error synchronizing connection - Hiba a kapcsolat szinkronizálása során + 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 a csoport hivatkozás frissítésekor + Hiba történt a csoporthivatkozás frissítésekor No comment provided by engineer. Error updating message - Hiba az üzenet frissítésekor + Hiba történt az üzenet frissítésekor No comment provided by engineer. + + Error updating server + Hiba történt a kiszolgáló frissítésekor + alert title + Error updating settings Hiba történt a beállítások frissítésekor @@ -2600,17 +3475,17 @@ Ez a művelet nem vonható vissza! Error updating user privacy - Hiba a felhasználói beállítások frissítésekor + Hiba történt a felhasználói adatvédelem frissítésekor No comment provided by engineer. Error uploading the archive - Hiba az archívum feltöltésekor + Hiba történt az archívum feltöltésekor No comment provided by engineer. Error verifying passphrase: - Hiba a jelmondat ellenőrzésekor: + Hiba történt a jelmondat hitelesítésekor: No comment provided by engineer. @@ -2621,18 +3496,30 @@ Ez a művelet nem vonható vissza! Error: %@ Hiba: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid - Hiba: az URL érvénytelen + Hiba: a webcím érvénytelen No comment provided by engineer. Error: no database file - Hiba: nincs adatbázis fájl + Hiba: nincs adatbázisfájl No comment provided by engineer. + + Errors + Hibák + No comment provided by engineer. + + + Errors in servers configuration. + Hibák a kiszolgálók konfigurációjában. + servers error + Even when disabled in the conversation. Akkor is, ha le van tiltva a beszélgetésben. @@ -2648,6 +3535,11 @@ Ez a művelet nem vonható vissza! Kibontás chat item action + + Expired + Lejárt + token status text + Export database Adatbázis exportálása @@ -2658,6 +3550,11 @@ Ez a művelet nem vonható vissza! 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. @@ -2670,7 +3567,7 @@ Ez a művelet nem vonható vissza! Exporting database archive… - Adatbázis archívum exportálása… + Adatbázis-archívum exportálása… No comment provided by engineer. @@ -2683,19 +3580,73 @@ Ez a művelet nem vonható vissza! 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 üzenet kézbesítés. + Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés. + No comment provided by engineer. + + + Faster sending messages. + Gyorsabb üzenetküldés. No comment provided by engineer. Favorite Kedvenc + swipe action + + + Favorites + Kedvencek No comment provided by engineer. + + File error + Fájlhiba + file error alert title + + + File errors: +%@ + Fájlhiba: +%@ + alert message + + + File is blocked by server operator: +%@. + A kiszolgáló üzemeltetője letiltotta a fájlt: +%@. + 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: %@ + file error text + + + File status + Fájl állapota + No comment provided by engineer. + + + File status: %@ + Fájl állapota: %@ + copied message info + File will be deleted from servers. - A fájl törölve lesz a kiszolgálóról. + A fájl törölve lesz a kiszolgálókról. No comment provided by engineer. @@ -2713,29 +3664,34 @@ Ez a művelet nem vonható vissza! Fájl: %@ No comment provided by engineer. + + Files + Fájlok + No comment provided by engineer. + Files & media - Fájlok és média + Fájlok és médiatartalmak No comment provided by engineer. Files and media - Fájlok és médiatartalom + Fájlok és médiatartalmak chat feature - - Files and media are prohibited in this group. - A fájlok- és a médiatartalom küldése le van tiltva ebben a csoportban. + + 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 - Fájlok és média tartalom küldése le van tiltva + A fájlok- és médiatartalmak nincsenek engedélyezve No comment provided by engineer. Files and media prohibited! - A fájlok- és a médiatartalom küldése le van tiltva! + A fájlok- és a médiatartalmak küldése le van tiltva! No comment provided by engineer. @@ -2785,7 +3741,7 @@ Ez a művelet nem vonható vissza! Fix not supported by contact - Ismerős általi javítás nem támogatott + Partner általi javítás nem támogatott No comment provided by engineer. @@ -2793,21 +3749,71 @@ Ez a művelet nem vonható vissza! Csoporttag általi javítás nem támogatott No comment provided by engineer. + + For all moderators + Az összes moderátor számára + No comment provided by engineer. + + + For chat profile %@: + A(z) %@ nevű csevegési profilhoz: + servers error + 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 + No comment provided by engineer. + Forward Továbbítás chat item action + + Forward %d message(s)? + Továbbít %d üzenetet? + alert title + Forward and save messages Üzenetek továbbítása és mentése No comment provided by engineer. + + Forward messages + Üzenetek továbbítása + alert action + + + Forward messages without files? + Továbbítja az üzeneteket fájlok nélkül? + alert message + + + 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 @@ -2815,9 +3821,43 @@ Ez a művelet nem vonható vissza! Forwarded from - Továbbítva innen: + 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Továbbítókiszolgáló: %1$@ +Célkiszolgáló-hiba: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Továbbítókiszolgáló: %1$@ +Hiba: %2$@ + snd error text + Found desktop Megtalált számítógép @@ -2835,27 +3875,22 @@ Ez a művelet nem vonható vissza! Full name (optional) - Teljes név (opcionális) - No comment provided by engineer. - - - Full name: - Teljes név: + Teljes név (nem kötelező) No comment provided by engineer. Fully decentralized – visible only to members. - Teljesen decentralizált - kizárólag tagok számára látható. + 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! + 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 + Tovább csökkentett akkumulátor-használat No comment provided by engineer. @@ -2863,6 +3898,21 @@ Ez a művelet nem vonható vissza! GIF-ek és matricák No comment provided by engineer. + + Get notified when mentioned. + Kapjon értesítést, ha megemlítik. + No comment provided by engineer. + + + Good afternoon! + Jó napot! + message preview + + + Good morning! + Jó reggelt! + message preview + Group Csoport @@ -2885,72 +3935,37 @@ Ez a művelet nem vonható vissza! Group full name (optional) - Csoport teljes neve (opcionális) + A csoport teljes neve (nem kötelező) No comment provided by engineer. Group image - Csoportkép + Csoport profilképe No comment provided by engineer. Group invitation - Csoportos meghívó + Csoportmeghívó No comment provided by engineer. Group invitation expired - A csoport meghívó lejárt + A csoportmeghívó lejárt No comment provided by engineer. Group invitation is no longer valid, it was removed by sender. - A csoport meghívó már nem érvényes, a küldője törölte. + A csoportmeghívó már nem érvényes, a küldője eltávolította. No comment provided by engineer. Group link - Csoport hivatkozás + Csoporthivatkozás No comment provided by engineer. Group links - Csoport hivatkozások - No comment provided by engineer. - - - Group members can add message reactions. - Csoporttagok üzenetreakciókat adhatnak hozzá. - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. (24 hours) - A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) - No comment provided by engineer. - - - Group members can send SimpleX links. - A csoport tagjai küldhetnek SimpleX hivatkozásokat. - No comment provided by engineer. - - - Group members can send direct messages. - A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. - No comment provided by engineer. - - - Group members can send disappearing messages. - A csoport tagjai küldhetnek eltűnő üzeneteket. - No comment provided by engineer. - - - Group members can send files and media. - A csoport tagjai küldhetnek fájlokat és médiatartalmakat. - No comment provided by engineer. - - - Group members can send voice messages. - A csoport tagjai küldhetnek hangüzeneteket. + Csoporthivatkozások No comment provided by engineer. @@ -2960,17 +3975,17 @@ Ez a művelet nem vonható vissza! Group moderation - Csoport moderáció + Csoport moderálása No comment provided by engineer. Group preferences - Csoport beállítások + Csoportbeállítások No comment provided by engineer. Group profile - Csoport profil + Csoportprofil No comment provided by engineer. @@ -2980,27 +3995,37 @@ Ez a művelet nem vonható vissza! Group welcome message - Csoport üdvözlő üzenete + A csoport üdvözlőüzenete No comment provided by engineer. Group will be deleted for all members - this cannot be undone! - Csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza! + 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örlésre kerül az ön számára - ez a művelet nem vonható vissza! + 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 - Segítség + 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 - Rejtett + Se név, se üzenet No comment provided by engineer. @@ -3010,12 +4035,12 @@ Ez a művelet nem vonható vissza! Hidden profile password - Rejtett profil jelszó + Rejtett profiljelszó No comment provided by engineer. Hide - Elrejt + Összecsukás chat item action @@ -3030,7 +4055,7 @@ Ez a művelet nem vonható vissza! Hide: - Elrejt: + Elrejtve: No comment provided by engineer. @@ -3040,7 +4065,7 @@ Ez a művelet nem vonható vissza! History is not sent to new members. - Az előzmények nem kerülnek elküldésre az új tagok számára. + Az előzmények nem lesznek elküldve az új tagok számára. No comment provided by engineer. @@ -3048,10 +4073,20 @@ Ez a művelet nem vonható vissza! Hogyan működik a SimpleX 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 + No comment provided by engineer. + How it works Hogyan működik - No comment provided by engineer. + alert button How to @@ -3060,12 +4095,12 @@ Ez a művelet nem vonható vissza! How to use it - Hogyan használja + Használati útmutató No comment provided by engineer. How to use your servers - Kiszolgálók használata + Hogyan használja a saját kiszolgálóit No comment provided by engineer. @@ -3078,29 +4113,34 @@ Ez a művelet nem vonható vissza! ICE-kiszolgálók (soronként egy) No comment provided by engineer. + + IP address + IP-cím + 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 során, vagy ossza meg a hivatkozást. + 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 törlődik! + 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 az önmegsemmisítő jelkódot megadásra kerül: + 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 a ** Csináld később** elemre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis áttelepítése). + 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 - Figyelmen kívül hagyás + Mellőzés No comment provided by engineer. @@ -3118,9 +4158,9 @@ Ez a művelet nem vonható vissza! Azonnal No comment provided by engineer. - - Immune to spam and abuse - Spam és visszaélések elleni védelem + + Immune to spam + Védett a kéretlen tartalommal szemben No comment provided by engineer. @@ -3130,7 +4170,7 @@ Ez a művelet nem vonható vissza! Import chat database? - Csevegési adatbázis importálása? + Importálja a csevegési adatbázist? No comment provided by engineer. @@ -3143,14 +4183,26 @@ Ez a művelet nem vonható vissza! 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. +More improvements are coming soon! + Továbbfejlesztett kézbesítés, csökkentett adatforgalom-használat. +További fejlesztések hamarosan! + No comment provided by engineer. + Improved message delivery - Továbbfejlesztett üzenetküldés + Továbbfejlesztett üzenetkézbesítés No comment provided by engineer. @@ -3170,7 +4222,7 @@ Ez a művelet nem vonható vissza! In reply to - Válasz neki + Válaszul erre No comment provided by engineer. @@ -3178,6 +4230,16 @@ Ez a művelet nem vonható vissza! Bejövő hívás csengőhangja No comment provided by engineer. + + Inappropriate content + Kifogásolt tartalom + report reason + + + Inappropriate profile + Kifogásolt profil + report reason + Incognito Inkognitó @@ -3185,17 +4247,17 @@ Ez a művelet nem vonható vissza! Incognito groups - Inkognitó csoportok + Inkognitócsoportok No comment provided by engineer. Incognito mode - Inkognitó mód + 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 védi személyes adatait azáltal, hogy minden ismerőshöz új véletlenszerű profilt használ. + Az inkognitómód úgy védi a személyes adatait, hogy az összes partneréhez új, véletlenszerű profilt használ. No comment provided by engineer. @@ -3215,7 +4277,7 @@ Ez a művelet nem vonható vissza! Incompatible database version - Nem kompatibilis adatbázis verzió + Nem kompatibilis adatbázis-verzió No comment provided by engineer. @@ -3248,23 +4310,53 @@ Ez a művelet nem vonható vissza! 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! - Az azonnali push értesítések elrejtésre kerülnek! + Az azonnali push-értesítések el lesznek rejtve! No comment provided by engineer. - - Instantly - Azonnal - No comment provided by engineer. - Interface - Felület + Kezelőfelület No comment provided by engineer. + + Interface colors + Kezelőfelület színei + No comment provided by engineer. + + + 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) + token status text + Invalid QR code Érvénytelen QR-kód @@ -3272,12 +4364,12 @@ Ez a művelet nem vonható vissza! Invalid connection link - Érvénytelen kapcsolati hivatkozás + Érvénytelen kapcsolattartási hivatkozás No comment provided by engineer. Invalid display name! - Érvénytelen megjelenítendő felhaszálónév! + Érvénytelen megjelenítendő név! No comment provided by engineer. @@ -3303,7 +4395,7 @@ Ez a művelet nem vonható vissza! Invalid server address! Érvénytelen kiszolgálócím! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4417,11 @@ Ez a művelet nem vonható vissza! 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 @@ -3337,22 +4434,22 @@ Ez a művelet nem vonható vissza! Irreversible message deletion is prohibited in this chat. - Ebben a csevegésben az üzenetek végleges törlése le van tiltva. + Az üzenetek végleges törlése le van tiltva ebben a csevegésben. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. - Ebben a csoportban az üzenetek végleges törlése le van tiltva. + + 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őprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük. + 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 kapcsolata régi adatbázis biztonsági mentést használt. + Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt. No comment provided by engineer. @@ -3362,10 +4459,15 @@ Ez a művelet nem vonható vissza! 3. The connection was compromised. Ez akkor fordulhat elő, ha: 1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak. -2. Az üzenet visszafejtése sikertelen volt, mert vagy az ismerőse régebbi adatbázis biztonsági mentést használt. +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. 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 (%@). @@ -3384,7 +4486,7 @@ Ez a művelet nem vonható vissza! Join Csatlakozás - No comment provided by engineer. + swipe action Join group @@ -3415,7 +4517,7 @@ Ez a művelet nem vonható vissza! Join your group? This is your link for group %@! Csatlakozik a csoportjához? -Ez az ön hivatkozása a(z) %@ csoporthoz! +Ez a saját hivatkozása a(z) %@ nevű csoporthoz! No comment provided by engineer. @@ -3425,7 +4527,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Keep - Megtart + Megtartás + alert action + + + Keep conversation + Beszélgetés megtartása No comment provided by engineer. @@ -3435,8 +4542,8 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Keep unused invitation? - Fel nem használt meghívó megtartása? - No comment provided by engineer. + Megtartja a fel nem használt meghívót? + alert title Keep your connections @@ -3445,12 +4552,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! KeyChain error - Kulcstároló hiba + Kulcstartóhiba No comment provided by engineer. Keychain error - Kulcstároló hiba + Kulcstartóhiba No comment provided by engineer. @@ -3470,7 +4577,17 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Leave - Elhagy + Elhagyás + swipe action + + + Leave chat + Csevegés elhagyása + No comment provided by engineer. + + + Leave chat? + Elhagyja a csevegést? No comment provided by engineer. @@ -3480,12 +4597,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Leave group? - Csoport elhagyása? + Elhagyja a csoportot? No comment provided by engineer. Let's talk in SimpleX Chat - Beszélgessünk a SimpleX Chat-ben + Beszélgessünk a SimpleX Chatben email subject @@ -3500,17 +4617,32 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Link mobile and desktop apps! 🔗 - Társítsa össze a mobil és az asztali alkalmazásokat! 🔗 + Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗 No comment provided by engineer. Linked desktop options - Összekapcsolt számítógép beállítások + Társított számítógép beállítások No comment provided by engineer. Linked desktops - Összekapcsolt számítógépek + Társított számítógépek + No comment provided by engineer. + + + List + Lista + swipe action + + + 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. @@ -3523,11 +4655,6 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Élő üzenetek No comment provided by engineer. - - Local - Helyi - No comment provided by engineer. - Local name Helyi név @@ -3548,11 +4675,6 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Zárolási mód No comment provided by engineer. - - Make a private connection - Privát kapcsolat létrehozása - No comment provided by engineer. - Make one message disappear Egy üzenet eltüntetése @@ -3563,24 +4685,14 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Tegye priváttá a profilját! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Győződjön meg arról, hogy a %@ szervercímek megfelelő formátumúak, sorszeparáltak és nem duplikáltak (%@). - 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 WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nem duplikáltak. - No comment provided by engineer. - - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Sokan kérdezték: *ha a SimpleX-nek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?* + 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 mindenki számára töröltként + Jelölje meg az összes tag számára töröltként No comment provided by engineer. @@ -3590,7 +4702,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Mark verified - Ellenőrzöttként jelölve + Hitelesítés No comment provided by engineer. @@ -3603,24 +4715,104 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Max. 30 másodperc, azonnal érkezett. No comment provided by engineer. + + Media & file servers + Média- és fájlkiszolgálók + No comment provided by engineer. + + + Medium + Közepes + blur media + Member Tag No comment provided by engineer. + + Member inactive + Inaktív tag + item status text + + + Member reports + Tagok jelentései + chat feature + + + 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 meg fog változni erre: "%@". A csoport minden tagja értesítést kap róla. + 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 meg fog változni erre: "%@". A tag új meghívást fog kapni. + 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 eltávolítása a csoportból - ez a művelet nem vonható vissza! + 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 SimpleX links. + A tagok küldhetnek SimpleX-hivatkozásokat. + 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 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. @@ -3630,14 +4822,34 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Message delivery receipts! - Üzenetkézbesítési bizonylatok! + Üzenetkézbesítési jelentések! No comment provided by engineer. + + Message delivery warning + Üzenetkézbesítési figyelmeztetés + item status text + Message draft Üzenetvázlat No comment provided by engineer. + + Message forwarded + Továbbított üzenet + item status text + + + 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. + item status description + + + Message queue info + Üzenetsorbaállítási információ + No comment provided by engineer. + Message reactions Üzenetreakciók @@ -3645,12 +4857,27 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Message reactions are prohibited in this chat. - Az üzenetreakciók ebben a csevegésben le vannak tiltva. + A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben. No comment provided by engineer. - - Message reactions are prohibited in this group. - Ebben a csoportban az üzenetreakciók le vannak tiltva. + + Message reactions are prohibited. + A reakciók hozzáadása az üzenetekhez le van tiltva. + 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. @@ -3658,9 +4885,19 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Az üzenet forrása titokban marad. No comment provided by engineer. + + Message status + Üzenet állapota + No comment provided by engineer. + + + Message status: %@ + Üzenet állapota: %@ + copied message info + Message text - Üzenet szövege + Név és üzenet No comment provided by engineer. @@ -3680,17 +4917,37 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Messages from %@ will be shown! - A(z) %@ által írt üzenetek megjelennek! + %@ összes üzenete meg fog jelenni! No comment provided by engineer. + + Messages in this chat will never be deleted. + Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve. + alert message + + + Messages received + Fogadott üzenetek + No comment provided by engineer. + + + Messages sent + Elküldött üzenetek + No comment provided by engineer. + + + Messages were deleted after you selected them. + Az üzeneteket törölték miután kijelölte őket. + alert message + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. - Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi. + 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 üzeneteket, fájlokat és hívásokat **végpontok közötti kvantumrezisztens titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi. + 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. @@ -3725,7 +4982,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Migrating database archive… - Adatbázis archívum migrálása… + Adatbázis-archívum átköltöztetése… No comment provided by engineer. @@ -3735,12 +4992,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Migration error: - Átköltöztetés hiba: + Á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 az aktuális 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). + 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. @@ -3748,9 +5005,9 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Az átköltöztetés befejeződött No comment provided by engineer. - - Migrations: %@ - Átköltöztetések: %@ + + Migrations: + Átköltöztetések: No comment provided by engineer. @@ -3760,14 +5017,19 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Moderated at - Moderálva lett ekkor: + Moderálva No comment provided by engineer. Moderated at: %@ - Moderálva lett ekkor: %@ + Moderálva: %@ copied message info + + More + Továbbiak + swipe action + More improvements are coming soon! Hamarosan további fejlesztések érkeznek! @@ -3778,25 +5040,30 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Megbízhatóbb hálózati kapcsolat. No comment provided by engineer. + + More reliable notifications + Megbízhatóbb értesítések + No comment provided by engineer. + Most likely this connection is deleted. - Valószínűleg ez a kapcsolat törlésre került. + Valószínűleg ez a kapcsolat törölve lett. item status description - - Most likely this contact has deleted the connection with you. - Valószínűleg ez az ismerős törölte önnel a kapcsolatot. - No comment provided by engineer. - Multiple chat profiles - Több csevegőprofil + Több csevegési profil No comment provided by engineer. Mute - Elnémítás - No comment provided by engineer. + Némítás + notification label action + + + Mute all + Összes némítása + notification label action Muted when inactive! @@ -3806,7 +5073,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Name Név - No comment provided by engineer. + swipe action Network & servers @@ -3815,14 +5082,29 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Network connection - Internetkapcsolat + Hálózati kapcsolat No comment provided by engineer. + + Network decentralization + Hálózati decentralizáció + No comment provided by engineer. + + + 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. + snd error text + 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 @@ -3833,19 +5115,39 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Hálózat állapota No comment provided by engineer. + + New + Új + token status text + New Passcode Új jelkód 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. + No comment provided by engineer. + New chat - Új beszélgetés + Új csevegés + No comment provided by engineer. + + + New chat experience 🎉 + Új csevegési élmény 🎉 No comment provided by engineer. New contact request - Új kapcsolattartási kérelem + Új meghívási kérés notification @@ -3853,26 +5155,31 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Új kapcsolat: notification - - New database archive - Új adatbázis-archívum - No comment provided by engineer. - New desktop app! - Új asztali alkalmazás! + Új számítógép-alkalmazás! No comment provided by engineer. New display name - Új megjelenítési név + Új megjelenítendő név No comment provided by engineer. + + New events + Új események + notification + 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 @@ -3888,6 +5195,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Új jelmondat… No comment provided by engineer. + + New server + Új kiszolgáló + No comment provided by engineer. + No Nem @@ -3898,14 +5210,29 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nincs alkalmazás jelszó Authentication unavailable + + 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 - Nem kerültek ismerősök kiválasztásra + Nincs partner kijelölve No comment provided by engineer. No contacts to add - Nincs hozzáadandó ismerős + Nincs hozzáadandó partner No comment provided by engineer. @@ -3915,9 +5242,14 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! No device token! - Nincs eszköztoken! + Nincs készüléktoken! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja. + item status description + No filtered chats Nincsenek szűrt csevegések @@ -3933,26 +5265,111 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nincsenek előzmények No comment provided by engineer. + + No info, try to reload + Nincs információ, próbálja meg újratölteni + No comment provided by engineer. + + + No media & file servers. + Nincsenek média- és fájlkiszolgálók. + servers error + + + No message + Nincs üzenet + No comment provided by engineer. + + + No message servers. + Nincsenek üzenet-kiszolgálók. + servers error + 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 No comment provided by engineer. + + 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. + servers error + + + No token! + Nincs token! + alert title + + + 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 + No comment provided by engineer. + + + Nothing to forward! + Nincs mit továbbítani! + alert title + Notifications Értesítések @@ -3963,13 +5380,28 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Az értesítések le vannak tiltva! No comment provided by engineer. + + Notifications error + Értesítési hiba + alert title + + + Notifications privacy + Értesítési adatvédelem + No comment provided by engineer. + + + Notifications status + Értesítések állapota + alert title + Now admins can: - delete members' messages. - disable members ("observer" role) - Most már az adminok is: + Most már az adminisztrátorok is: - törölhetik a tagok üzeneteit. -- letilthatnak tagokat ("megfigyelő" szerepkör) +- letilthatnak tagokat („megfigyelő” szerepkör) No comment provided by engineer. @@ -3979,124 +5411,148 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Off - Ki - No comment provided by engineer. + Kikapcsolva + blur media Ok Rendben - No comment provided by engineer. + alert button Old database Régi adatbázis No comment provided by engineer. - - Old database archive - Régi adatbázis archívum - No comment provided by engineer. - One-time invitation link - Egyszer használatos meghívó hivatkozás + Egyszer használható meghívó No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - A kapcsolódáshoz Onion kiszolgálókra lesz szükség. VPN engedélyezése szükséges. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Onion-kiszolgálók **szükségesek** a kapcsolódáshoz. +Kompatibilis VPN szükséges. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion kiszolgálók használata, ha azok rendelkezésre állnak. VPN engedélyezése szükséges. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion-kiszolgálók használata, ha azok rendelkezésre állnak. +VPN engedélyezése szükséges. No comment provided by engineer. Onion hosts will not be used. - Onion kiszolgálók nem lesznek használva. + Az onion-kiszolgálók nem lesznek használva. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. - Csak a klienseszközök tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végponttól-végpontig titkosítással** küldött üzeneteket. + + 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 csoporttulajdonosok módosíthatják a csoportbeállításokat. + 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 csoporttulajdonosok engedélyezhetik a fájlok- és a médiatartalmak küldését. + 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 csoporttulajdonosok engedélyezhetik a hangüzenetek küldését. + 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á üzenetreakciókat. + 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 (ismerőse csak törlésre jelölheti meg őket ). (24 óra) + 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. + 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. + 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. + Csak Ön tud hangüzeneteket küldeni. No comment provided by engineer. Only your contact can add message reactions. - Csak az ismerős tud üzeneteakciókat adni. + 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 az ismerőse tudja az üzeneteket véglegesen törölni (ön csak törlésre jelölheti meg azokat). (24 óra) + 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 az ismerős tud hívást indítani. + Csak a partnere tud hívást indítani. No comment provided by engineer. Only your contact can send disappearing messages. - Csak az ismerős tud eltűnő üzeneteket küldeni. + Csak a partnere tud eltűnő üzeneteket küldeni. No comment provided by engineer. Only your contact can send voice messages. - Csak az ismerős tud hangüzeneteket küldeni. + Csak a partnere tud hangüzeneteket küldeni. No comment provided by engineer. Open Megnyitás - No comment provided by engineer. + alert action Open Settings Beállítások megnyitása No comment provided by engineer. + + Open changes + Módosítások megtekintése + No comment provided by engineer. + Open chat Csevegés megnyitása @@ -4104,34 +5560,48 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Open chat console - Csevegés konzol megnyitása + Csevegési konzol megnyitása authentication reason + + Open conditions + Feltételek megnyitása + No comment provided by engineer. + Open group Csoport megnyitása No comment provided by engineer. + + Open link? + alert title + Open migration to another device - Átköltöztetés megkezdése egy másik eszközre + Átköltöztetés indítása egy másik eszközre authentication reason - - Open user profiles - Felhasználói profilok megnyitása - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Nyílt forráskódú protokoll és forráskód – bárki üzemeltethet kiszolgálókat. - No comment provided by engineer. - Opening app… Az alkalmazás megnyitása… No comment provided by engineer. + + Operator + Üzemeltető + No comment provided by engineer. + + + Operator server + Kiszolgáló-üzemeltető + alert title + + + 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 @@ -4144,7 +5614,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Or securely share this file link - Vagy a fájl hivítkozásának biztonságos megosztása + Vagy ossza meg biztonságosan ezt a fájlhivatkozást No comment provided by engineer. @@ -4152,19 +5622,36 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! 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 További No comment provided by engineer. + + Other file errors: +%@ + Egyéb fájlhiba: +%@ + alert message + PING count - PING számláló + PING-ek száma No comment provided by engineer. PING interval - PING időköze + Időtartam a PING-ek között No comment provided by engineer. @@ -4174,7 +5661,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Passcode changed! - A jelkód megváltozott! + A jelkód módosult! No comment provided by engineer. @@ -4184,7 +5671,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Passcode not changed! - A jelkód nem változott! + A jelkód nem módosult! No comment provided by engineer. @@ -4192,19 +5679,24 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! A jelkód beállítva! No comment provided by engineer. + + Password + Jelszó + No comment provided by engineer. + Password to show - Jelszó megjelenítése + Jelszó a megjelenítéshez No comment provided by engineer. Past member %@ - Korábbi csoport tag %@ + (Már nem tag) %@ past/unknown group member Paste desktop address - Számítógép azonosítójának beillesztése + Számítógép címének beillesztése No comment provided by engineer. @@ -4219,17 +5711,17 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Paste the link you received - Fogadott hivatkozás beillesztése + Kapott hivatkozás beillesztése No comment provided by engineer. - - People can connect to you only via the links you share. - Az emberek csak az ön által megosztott hivatkozáson keresztül kapcsolódhatnak. + + Pending + Függőben No comment provided by engineer. - - Periodically - Rendszeresen + + Periodic + Időszakos No comment provided by engineer. @@ -4242,29 +5734,46 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Kép a képben hívások 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. - Ismerős felkérése, hogy engedélyezze a hangüzenetek küldését. + 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. +Please 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. +Minden 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 ismerősét, hogy küldjön egy másikat. + 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 hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra. + 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 az ön és ismerőse beállításait. + 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őek-e ehhez az eszközhöz. + Ellenőrizze, hogy a hálózati beállítások megfelelők-e ehhez az eszközhöz. No comment provided by engineer. @@ -4276,22 +5785,22 @@ Hiba: %@ Please contact group admin. - Lépjen kapcsolatba a csoport adminnal. + Lépjen kapcsolatba a csoport adminisztrátorával. No comment provided by engineer. Please enter correct current passphrase. - Adja meg a helyes aktuális jelmondatát. + 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. - Előző jelszó megadása az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza. + 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! + Jegyezze fel vagy tárolja el biztonságosan – az elveszett jelkódot nem lehet visszaállítani! No comment provided by engineer. @@ -4301,7 +5810,7 @@ Hiba: %@ 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. + 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. @@ -4311,14 +5820,34 @@ Hiba: %@ 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 megváltoztatni. + Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja módosítani. No comment provided by engineer. + + 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. + token info + Polish interface Lengyel kezelőfelület No comment provided by engineer. + + Port + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen @@ -4329,14 +5858,14 @@ Hiba: %@ Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. No comment provided by engineer. - - Preset server - Előre beállított kiszolgáló - No comment provided by engineer. - Preset server address - Előre beállított kiszolgáló címe + 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. @@ -4344,19 +5873,54 @@ Hiba: %@ 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 - Adatvédelem újraértelmezve + Ú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ájl nevek + 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 🚀 No comment provided by engineer. @@ -4364,6 +5928,16 @@ Hiba: %@ Privát jegyzetek name of notes to self + + 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 @@ -4379,69 +5953,74 @@ Hiba: %@ Profilképek No comment provided by engineer. - - Profile name - Profilnév - No comment provided by engineer. - - - Profile name: - Profil neve: - No comment provided by engineer. - Profile password Profiljelszó No comment provided by engineer. + + Profile theme + Profiltéma + No comment provided by engineer. + Profile update will be sent to your contacts. - A profilfrissítés elküldésre került az ismerősök számára. - No comment provided by engineer. + A profilfrissítés el lesz küldve a partnerei számára. + alert message Prohibit audio/video calls. - Hang- és videóhívások tiltása. + A hívások kezdeményezése le van tiltva. No comment provided by engineer. Prohibit irreversible message deletion. - Az üzenetek véglegesen való törlése le van tiltva. + Az elküldött üzenetek végleges törlése le van tiltva. No comment provided by engineer. Prohibit message reactions. - Üzenetreakciók tiltása. + A reakciók hozzáadása az üzenethez le van tiltva. No comment provided by engineer. Prohibit messages reactions. - Az üzenetreakciók tiltása. + 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 SimpleX links. - A SimpleX hivatkozások küldése le van tiltva. + A SimpleX-hivatkozások küldése le van tiltva. No comment provided by engineer. Prohibit sending direct messages to members. - Közvetlen üzenetek küldésének letiltása a tagok számára. + A közvetlen üzenetek küldése a tagok között le van tiltva. No comment provided by engineer. Prohibit sending disappearing messages. - Eltűnő üzenetek küldésének letiltása. + Az eltűnő üzenetek küldése le van tiltva. No comment provided by engineer. Prohibit sending files and media. - Fájlok- és a médiatartalom küldés letiltása. + A fájlok- és a médiatartalmak küldése le van tiltva. No comment provided by engineer. Prohibit sending voice messages. - Hangüzenetek küldésének letiltása. + A hangüzenetek küldése le van tiltva. + No comment provided by engineer. + + + Protect IP address + IP-cím védelme No comment provided by engineer. @@ -4449,34 +6028,56 @@ Hiba: %@ Alkalmazás képernyőjének védelme No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben. +Engedélyezze a *Hálózat és kiszolgálók* menüben. + No comment provided by engineer. + Protect your chat profiles with a password! - Csevegési profiljok védelme jelszóval! + Védje meg a csevegési profiljait egy jelszóval! No comment provided by engineer. Protocol timeout - Protokoll időtúllépés + Protokoll időtúllépése No comment provided by engineer. Protocol timeout per KB - Protokoll időkorlát KB-onként + 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 + Push-értesítések No comment provided by engineer. Push server - Push kiszolgáló + Push-kiszolgáló No comment provided by engineer. Quantum resistant encryption - Kvantumrezisztens titkosítás + Kvantumbiztos titkosítás No comment provided by engineer. @@ -4484,6 +6085,11 @@ Hiba: %@ Értékelje az alkalmazást No comment provided by engineer. + + Reachable chat toolbar + Könnyen elérhető eszköztár + No comment provided by engineer. + React… Reagálj… @@ -4491,34 +6097,29 @@ Hiba: %@ Read - Olvasd el - No comment provided by engineer. + Olvasott + swipe action Read more Tudjon meg többet No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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. - További információ a GitHub tárolónkban. - 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). @@ -4526,47 +6127,62 @@ Hiba: %@ Receipts are disabled - Üzenet kézbesítési jelentés letiltva + 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 at - Fogadva ekkor: + Fogadva No comment provided by engineer. Received at: %@ - Fogadva ekkor: %@ + Fogadva: %@ copied message info Received file event - Fogadott fájl esemény + Fogadott fájlesemény notification Received message - Fogadott üzenet + Fogadott üzenetbuborék színe message info title + + 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ó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be. - No comment provided by engineer. - - - Receiving concurrency - Egyidejű fogadás + 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állt. + A fájl fogadása le fog állni. No comment provided by engineer. Receiving via - Fogadás a + Fogadás a következőn keresztül: No comment provided by engineer. @@ -4581,38 +6197,79 @@ Hiba: %@ Recipients see updates as you type them. - A címzettek a beírás közben látják a frissítéseket. + 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. - Újrakapcsolódás az összes kiszolgálóhoz az üzenetek kézbesítésének kikényszerítéséhez. Ez további forgalmat használ. + 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ódás a kiszolgálókhoz? + Újrakapcsolódik a kiszolgálókhoz? No comment provided by engineer. Record updated at - A bejegyzés frissítve + Bejegyzés frissítve No comment provided by engineer. Record updated at: %@ - A bejegyzés frissítve: %@ + Bejegyzés frissítve: %@ copied message info Reduced battery usage - Csökkentett akkumulátorhasználat + Csökkentett akkumulátor-használat No comment provided by engineer. + + Register + Regisztrálás + No comment provided by engineer. + + + Register notification token? + Regisztrálja az értesítési tokent? + token info + + + Registered + Regisztrálva + token status text + Reject Elutasítás - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4621,17 +6278,17 @@ Hiba: %@ Reject contact request - Kapcsolatfelvételi kérelem elutasítása + Meghívási kérés elutasítása No comment provided by engineer. Relay server is only used if necessary. Another party can observe your IP address. - Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címét. + 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. - Az átjátszó kiszolgáló megvédi IP-címét, de megfigyelheti a hívás időtartamát. + 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. @@ -4639,24 +6296,34 @@ Hiba: %@ 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 - Tag eltávolítása + Eltávolítás No comment provided by engineer. Remove member? - Tag eltávolítása? + Eltávolítja a tagot? No comment provided by engineer. Remove passphrase from keychain? - Jelmondat eltávolítása a kulcstárolóból? + Eltávolítja a jelmondatot a kulcstartóból? No comment provided by engineer. Renegotiate - Újraegyzetetés + Újraegyeztetés No comment provided by engineer. @@ -4666,12 +6333,12 @@ Hiba: %@ Renegotiate encryption? - Titkosítás újraegyeztetése? + Újraegyezteti a titkosítást? No comment provided by engineer. Repeat connection request? - Kapcsolódási kérés megismétlése? + Megismétli a meghívási kérést? No comment provided by engineer. @@ -4686,7 +6353,7 @@ Hiba: %@ Repeat join request? - Csatlakozási kérés megismétlése? + Megismétli a meghívási kérést? No comment provided by engineer. @@ -4699,24 +6366,99 @@ Hiba: %@ Válasz chat item action + + Report + Jelentés + chat item action + + + 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. + report reason + + + Report reason? + Jelentés indoklása? + No comment provided by engineer. + + + 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 reason + + + Report: %@ + Jelentés: %@ + report in notification + + + 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 + No comment provided by engineer. + Required - Megkövetelt + Szükséges No comment provided by engineer. Reset - Alaphelyzetbe állítás + 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 alaphelyzetbe állítása + 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 - Alaphelyzetbe állítás + 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. @@ -4741,12 +6483,12 @@ Hiba: %@ Restore database backup? - Adatbázismentés visszaállítása? + Visszaállítja az adatbázismentést? No comment provided by engineer. Restore database error - Hiba az adatbázis visszaállításakor + Hiba történt az adatbázis visszaállításakor No comment provided by engineer. @@ -4759,9 +6501,9 @@ Hiba: %@ Felfedés chat item action - - Revert - Visszaállít + + Review conditions + Feltételek felülvizsgálata No comment provided by engineer. @@ -4776,7 +6518,7 @@ Hiba: %@ Revoke file? - Fájl visszavonása? + Visszavonja a fájlt? No comment provided by engineer. @@ -4789,9 +6531,19 @@ Hiba: %@ Csevegési szolgáltatás indítása No comment provided by engineer. - - SMP servers - Üzenetküldő (SMP) kiszolgálók + + SMP server + SMP-kiszolgáló + No comment provided by engineer. + + + SOCKS proxy + SOCKS-proxy + No comment provided by engineer. + + + Safely receive files + Fájlok biztonságos fogadása No comment provided by engineer. @@ -4802,61 +6554,62 @@ Hiba: %@ Save Mentés - chat item action + alert button +chat item action Save (and notify contacts) - Mentés (és az ismerősök értesítése) - No comment provided by engineer. + Mentés (és a partnerek értesítése) + alert button Save and notify contact - Mentés és ismerős értesítése - No comment provided by engineer. + Mentés és a partner értesítése + alert button 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 csoport profil frissítése - No comment provided by engineer. - - - Save archive - Archívum mentése - No comment provided by engineer. - - - Save auto-accept settings - Automatikus elfogadási beállítások mentése + Mentés és a csoportprofil frissítése No comment provided by engineer. Save group profile - Csoport profil elmentése + Csoportprofil mentése + No comment provided by engineer. + + + Save list + Lista mentése No comment provided by engineer. Save passphrase and open chat - Jelmondat elmentése és csevegés megnyitása + Jelmondat mentése és a csevegés megnyitása No comment provided by engineer. Save passphrase in Keychain - Jelmondat mentése a kulcstárban + Jelmondat mentése a kulcstartóba No comment provided by engineer. Save preferences? - Beállítások mentése? - No comment provided by engineer. + Menti a beállításokat? + alert title Save profile password - Felhasználói fiók jelszavának mentése + Profiljelszó mentése No comment provided by engineer. @@ -4866,19 +6619,19 @@ Hiba: %@ Save servers? - Kiszolgálók mentése? - No comment provided by engineer. - - - Save settings? - Beállítások mentése? - No comment provided by engineer. + Menti a kiszolgálókat? + alert title Save welcome message? - Üdvözlőszöveg mentése? + Menti az üdvözlőüzenetet? No comment provided by engineer. + + Save your profile? + Menti a profilt? + alert title + Saved Mentett @@ -4886,12 +6639,12 @@ Hiba: %@ Saved WebRTC ICE servers will be removed - A mentett WebRTC ICE kiszolgálók eltávolításra kerülnek + A mentett WebRTC ICE-kiszolgálók el lesznek távolítva No comment provided by engineer. Saved from - Mentve innen: + Elmentve innen No comment provided by engineer. @@ -4899,6 +6652,21 @@ Hiba: %@ Mentett üzenet message info title + + 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 QR code QR-kód beolvasása @@ -4906,17 +6674,17 @@ Hiba: %@ Scan QR code from desktop - QR-kód beolvasása számítógépről + QR-kód beolvasása a számítógépről No comment provided by engineer. Scan code - Kód beolvasása + Beolvasás No comment provided by engineer. Scan security code from your contact's app. - Biztonsági kód beolvasása ismerős általi alkalmazásból. + Biztonsági kód beolvasása a partnere alkalmazásából. No comment provided by engineer. @@ -4931,19 +6699,29 @@ Hiba: %@ Search bar accepts invitation links. - A keresősáv elfogadja a meghívó hivatkozásokat. + 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 + Keresés vagy SimpleX-hivatkozás beillesztése + No comment provided by engineer. + + + Secondary + Másodlagos szín No comment provided by engineer. Secure queue - Biztonságos várólista + Biztonságos sorba állítás server test step + + Secured + Biztosítva + No comment provided by engineer. + Security assessment Biztonsági kiértékelés @@ -4956,7 +6734,22 @@ Hiba: %@ Select - Választás + Kijelölés + chat item action + + + 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. @@ -4966,17 +6759,17 @@ Hiba: %@ Self-destruct passcode - Önmegsemmisítési jelkód + Önmegsemmisítő-jelkód No comment provided by engineer. Self-destruct passcode changed! - Az önmegsemmisítési jelkód megváltozott! + Az önmegsemmisítő-jelkód módosult! No comment provided by engineer. Self-destruct passcode enabled! - Az önmegsemmisítési jelkód engedélyezve! + Az önmegsemmisítő-jelkód engedélyezve! No comment provided by engineer. @@ -4986,7 +6779,7 @@ Hiba: %@ Send a live message - it will update for the recipient(s) as you type it - Élő üzenet küldése - a címzett(ek) számára frissül, ahogy beírja + É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. @@ -4994,11 +6787,6 @@ Hiba: %@ 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 @@ -5009,9 +6797,14 @@ Hiba: %@ 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ézetek küldése + Hivatkozás előnézete No comment provided by engineer. @@ -5019,124 +6812,219 @@ Hiba: %@ É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 notifications: - Értesítések küldése: + + Send private reports + Privát jelentések küldése No comment provided by engineer. Send questions and ideas - Ötletek és kérdések beküldése + Ötletek és javaslatok No comment provided by engineer. Send receipts - Üzenet kézbesítési jelentések + 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 galériából vagy egyedi billentyűzetekről. + 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. - Az utolsó 100 üzenet elküldése az új tagoknak. + Legfeljebb az utolsó 100 üzenet elküldése az új tagok számára. No comment provided by engineer. Sender cancelled file transfer. - A küldő megszakította a fájl átvitelt. - No comment provided by engineer. + A fájl küldője visszavonta az átvitelt. + alert message Sender may have deleted the connection request. - A küldő törölhette a kapcsolódási kérelmet. + 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élyezésre kerül az összes látható csevegési profilban lévő minden ismerős számára. + 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és küldése minden ismerős számára engedélyezésre kerül. + 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állt. + 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 küldése le van tiltva %lld ismerősnél + 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 küldése le van tiltva %lld csoportban + 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 küldése engedélyezve van %lld ismerős számára + 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 küldése engedélyezve van %lld csoportban + A kézbesítési jelentések engedélyezve vannak %lld csoportban No comment provided by engineer. Sending via - Küldés ezen keresztül + Küldés a következőn keresztül: No comment provided by engineer. Sent at - Elküldve ekkor: + Elküldve No comment provided by engineer. Sent at: %@ - Elküldve ekkor: %@ + Elküldve: %@ copied message info + + Sent directly + Közvetlenül küldött + No comment provided by engineer. + Sent file event - Elküldött fájl esemény + Elküldött fájlesemény notification Sent message - Elküldött üzenet + Üzenetbuborék színe message info title + + 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örlésre kerülnek a beállított idő után. + 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ó + No comment provided by engineer. + + + Server added to operator %@. + Kiszolgáló hozzáadva a következő üzemeltetőhöz: %@. + alert message + + + 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: %@. + No comment provided by engineer. + + + Server operator changed. + A kiszolgáló üzemeltetője módosult. + alert title + + + Server operators + Kiszolgálóüzemeltetők + No comment provided by engineer. + + + Server protocol changed. + A kiszolgáló-protokoll módosult. + alert title + Server requires authorization to create queues, check password - A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát + 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 engedélyre van szüksége a várólisták feltöltéséhez, ellenőrizze jelszavát + A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát server test error Server test failed! - Sikertelen kiszolgáló-teszt! + Sikertelen kiszolgáló teszt! + No comment provided by engineer. + + + Server type + Kiszolgáló típusa + No comment provided by engineer. + + + Server version is incompatible with network settings. + A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal. + srv error text + + + Server version is incompatible with your app: %@. + A kiszolgáló verziója nem kompatibilis az alkalmazással: %@. No comment provided by engineer. @@ -5144,6 +7032,16 @@ Hiba: %@ 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 @@ -5154,9 +7052,19 @@ Hiba: %@ 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… - Ismerős nevének beállítása… + 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. @@ -5166,7 +7074,12 @@ Hiba: %@ Set it instead of system authentication. - Rendszerhitelesítés helyetti beállítás. + 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. No comment provided by engineer. @@ -5186,7 +7099,7 @@ Hiba: %@ Set the message shown to new members! - Megjelenő üzenet beállítása az új tagok számára! + Megjelenítendő üzenet beállítása az új tagok számára! No comment provided by engineer. @@ -5199,44 +7112,85 @@ Hiba: %@ Beállítások No comment provided by engineer. + + Settings were changed. + A beállítások módosultak. + alert message + Shape profile images - Profilkép alakzat + Profilkép alakzata No comment provided by engineer. Share Megosztás - chat item action + alert action +chat item action Share 1-time link - Egyszer használatos hivatkozás megosztása + 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 SimpleX address on social media. + SimpleX-cím megosztása a közösségi médiában. No comment provided by engineer. Share address - Azonosító megosztása + Cím megosztása + No comment provided by engineer. + + + Share address publicly + Cím nyilvános megosztása No comment provided by engineer. Share address with contacts? - Megosztja az azonosítót az ismerőseivel? + Megosztja a címet a partnereivel? + alert title + + + Share from other apps. + Megosztás más alkalmazásokból. No comment provided by engineer. Share link - Hivatkozás megosztása + Megosztás + No comment provided by engineer. + + + Share profile + Profil megosztása No comment provided by engineer. Share this 1-time invite link - Egyszer használatos meghívó hivatkozás megosztása + 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 ismerősökkel + Megosztás a partnerekkel + No comment provided by engineer. + + + Short link + Rövid hivatkozás No comment provided by engineer. @@ -5256,22 +7210,47 @@ Hiba: %@ Show last messages - Utolsó üzenetek megjelenítése + 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 - Előnézet megjelenítése + Értesítés előnézete + 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: - Megjelenítés: + Megjelenítve: + No comment provided by engineer. + + + SimpleX + SimpleX No comment provided by engineer. SimpleX Address - SimpleX azonosító + SimpleX-cím + 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. @@ -5281,32 +7260,47 @@ Hiba: %@ SimpleX Lock - SimpleX zárolás + SimpleX-zár No comment provided by engineer. SimpleX Lock mode - SimpleX zárolási mód + Zárolási mód No comment provided by engineer. SimpleX Lock not enabled! - SimpleX zárolás nincs engedélyezve! + A SimpleX-zár nincs bekapcsolva! No comment provided by engineer. SimpleX Lock turned on - SimpleX zárolás bekapcsolva + SimpleX-zár bekapcsolva No comment provided by engineer. SimpleX address - SimpleX azonosító + 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ó? + No comment provided by engineer. + + + SimpleX channel link + SimpleX-csatornahivatkozás + simplex link type + SimpleX contact address - SimpleX kapcsolattartási azonosító + SimpleX kapcsolattartási cím simplex link type @@ -5316,32 +7310,42 @@ Hiba: %@ SimpleX group link - SimpleX csoport hivatkozás + SimpleX-csoporthivatkozás simplex link type SimpleX links - SimpleX hivatkozások + SimpleX-hivatkozások chat feature - - SimpleX links are prohibited in this group. - A SimpleX hivatkozások küldése ebben a csoportban le van tiltva. + + 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 + A SimpleX-hivatkozások küldése le van tiltva No comment provided by engineer. SimpleX one-time invitation - SimpleX egyszer használatos meghívó + Egyszer használható SimpleX-meghívó simplex link type + + 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 inkognító mód + Egyszerűsített inkognitómód + No comment provided by engineer. + + + Size + Méret No comment provided by engineer. @@ -5359,16 +7363,49 @@ Hiba: %@ Kis csoportok (max. 20 tag) No comment provided by engineer. + + Soft + Enyhe + blur media + + + 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ás során – további részletekért a csevegési konzolban olvashat. + 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + Néhány kiszolgáló megbukott a teszten: +%@ + alert message + Somebody Valaki notification title + + Spam + Kéretlen tartalom + blocking reason +report reason + Square, circle, or anything in between. Négyzet, kör vagy bármi a kettő között. @@ -5381,7 +7418,7 @@ Hiba: %@ Start chat? - Csevegés indítása? + Elindítja a csevegést? No comment provided by engineer. @@ -5389,6 +7426,16 @@ Hiba: %@ Átköltöztetés indítása No comment provided by engineer. + + Starting from %@. + Statisztikagyűjtés kezdete: %@. + No comment provided by engineer. + + + Statistics + Statisztikák + No comment provided by engineer. + Stop Megállítás @@ -5404,19 +7451,14 @@ Hiba: %@ Csevegési szolgáltatás megállítása No comment provided by engineer. - - Stop chat to enable database actions - Csevegés megállítása az adatbázis-műveletek engedélyezéséhez - 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ő adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni. + 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? - Csevegési szolgáltatás megállítása? + Megállítja a csevegést? No comment provided by engineer. @@ -5426,37 +7468,72 @@ Hiba: %@ Stop receiving file? - Fájl fogadás megállítása? + Megállítja a fájlfogadást? No comment provided by engineer. Stop sending file? - Fájl küldés megállítása? + Megállítja a fájlküldést? No comment provided by engineer. Stop sharing Megosztás megállítása - No comment provided by engineer. + alert action Stop sharing address? - Címmegosztás megállítása? - No comment provided by engineer. + Megállítja a címmegosztást? + alert title Stopping chat Csevegés megállítása folyamatban No comment provided by engineer. + + Storage + Tárhely + No comment provided by engineer. + + + Strong + Erős + blur media + 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 - Támogassa a SimpleX Chatet + 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. @@ -5466,12 +7543,22 @@ Hiba: %@ System authentication - Rendszerhitelesítés + Rendszer-hitelesítés + No comment provided by engineer. + + + TCP connection + TCP-kapcsolat No comment provided by engineer. TCP connection timeout - TCP kapcsolat időtúllépés + 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. @@ -5489,9 +7576,19 @@ Hiba: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Farok + No comment provided by engineer. + Take picture - Fotó készítése + Kép készítése + 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. @@ -5501,7 +7598,7 @@ Hiba: %@ Tap to Connect - Koppintson a kapcsolódáshoz + Koppintson ide a kapcsolódáshoz No comment provided by engineer. @@ -5511,34 +7608,39 @@ Hiba: %@ Tap to join - Koppintson a csatlakozáshoz + Koppintson ide a csatlakozáshoz No comment provided by engineer. Tap to join incognito - Koppintson az inkognitóban való csatlakozáshoz + Koppintson ide az inkognitóban való kapcsolódáshoz No comment provided by engineer. Tap to paste link - Koppintson a hivatkozás beillesztéséhez + Koppintson ide a hivatkozás beillesztéséhez No comment provided by engineer. Tap to scan - Koppintson a beolvasáshoz + Koppintson ide a QR-kód beolvasásához No comment provided by engineer. - - Tap to start a new chat - Koppintson az új csevegés indításához - No comment provided by engineer. + + Temporary file error + Ideiglenes fájlhiba + file error alert title Test failed at step %@. - A teszt sikertelen volt a(z) %@ lépésnél. + A teszt a(z) %@ lépésnél sikertelen volt. server test failure + + Test notifications + Értesítések tesztelése + No comment provided by engineer. + Test server Kiszolgáló tesztelése @@ -5552,7 +7654,7 @@ Hiba: %@ Tests failed! Sikertelen tesztek! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5561,54 +7663,64 @@ Hiba: %@ 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 – [hozzájárulás a Weblate-en keresztül](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 - hozzájárulás a Weblaten! - No comment provided by engineer. - - - The 1st platform without any user identifiers – private by design. - Az első csevegési rendszer bármiféle felhasználó azonosító nélkül - privátra lett tervezre. + Köszönet a felhasználóknak a Weblate-en való közreműködésért! No comment provided by engineer. 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. - A következő üzenet azonosítója hibás (kisebb vagy egyenlő az előzővel). -Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. + 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ő. 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 kapcsolatfelvételi kéréseket kap – beállítások megnyitása az engedélyezéshez. + 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 megváltoztatására tett kísérlet nem fejeződött be. + 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 kód nem egy SimpleX hivatkozás QR-kód. + 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 kapcsolat megszakad! + 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! - Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! + 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üpontban érhető el. + 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. @@ -5616,34 +7728,49 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet! No comment provided by engineer. + + The future of messaging + Az üzenetváltás jövője + No comment provided by engineer. + The hash of the previous message is different. - Az előző üzenet hash-e más. + Az előző üzenet hasítóértéke különbözik. No comment provided by engineer. The message will be deleted for all members. - Az üzenet minden tag számára törlésre kerül. + 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 minden tag számára moderáltként lesz megjelölve. + Az üzenet az összes tag számára moderáltként lesz megjelölve. No comment provided by engineer. - - The next generation of private messaging - A privát üzenetküldés következő generációja + + 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 került eltávolításra az átköltöztetés során, így törölhető. + A régi adatbázis nem lett eltávolítva az átköltöztetéskor, ezért törölhető. No comment provided by engineer. - - The profile is only shared with your contacts. - Profilja csak az ismerőseivel kerül megosztásra. + + 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. @@ -5658,42 +7785,62 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The servers for new connections of your current chat profile **%@**. - Jelenlegi profil új ismerőseinek kiszolgálói **%@**. + 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. + A beillesztett szöveg nem egy SimpleX-hivatkozás. No comment provided by engineer. - - Theme - Téma + + 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 a jelenlegi **%@** profiljára vonatkoznak. + 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óak az ismerős- és csoportbeállításokban. + 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édiatartalommal együtt törlésre kerülnek. Az alacsony felbontású fotók viszont megmaradnak. + 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 kiválasztottnál korábban küldött és fogadott üzenetek törlésre kerülnek. Ez több percet is igénybe vehet. + 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. No comment provided by engineer. + + 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. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - Ez a művelet nem vonható vissza - profiljai, ismerősei, üzenetei és fájljai visszafordíthatatlanul törlésre kerülnek. + Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek. No comment provided by engineer. @@ -5703,7 +7850,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This chat is protected by quantum resistant end-to-end encryption. - Ez a csevegés végpontok közötti kvantumrezisztens tikosítással védett. + Ez a csevegés végpontok közötti kvantumbiztos titkosítással védett. E2EE info chat item @@ -5713,12 +7860,12 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This display name is invalid. Please choose another name. - Ez a megjelenített felhasználónév érvénytelen. Válasszon egy másik nevet. + 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 kerülnek elküldésre. + 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. @@ -5728,17 +7875,37 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This is your own SimpleX address! - Ez a SimpleX azonosítója! + Ez a saját SimpleX-címe! No comment provided by engineer. This is your own one-time link! - Ez az egyszer használatos hivatkozása! + Ez a saját egyszer használható meghívója! + 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 a jelenlegi **%@** profiljában lévő üzenetekre érvényes. + 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. @@ -5748,7 +7915,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. To connect, your contact can scan QR code or use the link in the app. - A kapcsolódáshoz az ismerőse beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást. + 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. @@ -5761,21 +7928,46 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Új kapcsolat létrehozásához 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. - Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználói azonosítók helyett, a SimpleX üzenetsorokhoz rendel azonosítókat, minden egyes ismerőshöz egy különbözőt. + + 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édelme érdekében a kép-/hangfájlok UTC-t használnak. + 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 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. - Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót. -A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére. + 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. + 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. @@ -5785,37 +7977,72 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Rejtett profilja feltárásához írja be a teljes jelszót a keresőmezőbe a **Csevegési profiljai** oldalon. + 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 migrálása szükséges. + 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 ellenőrzéséhez ismerősével hasonlítsa össze (vagy szkennelje be) az eszközén lévő kódot. + 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ó mód kapcsolódáskor. + Inkognitóra váltás kapcsolódáskor. + No comment provided by engineer. + + + Token status: %@. + Token állapota: %@. + token status + + + Toolbar opacity + Eszköztár átlátszatlansága + No comment provided by engineer. + + + Total + Összes kapcsolat No comment provided by engineer. Transport isolation - Kapcsolat izolációs mód + Á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 ismerőstől érkező üzenetek fogadására szolgál (hiba: %@). + 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 ismerőstől érkező üzenetek fogadására szolgál. + 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. @@ -5845,7 +8072,7 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Unblock for all - Letiltás feloldása mindenki számára + Feloldás No comment provided by engineer. @@ -5855,18 +8082,18 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Unblock member for all? - Mindenki számára feloldja a tag letiltását? + Az összes tag számára feloldja a tag letiltását? No comment provided by engineer. Unblock member? - Tag feloldása? + Feloldja a tag letiltását? No comment provided by engineer. - - Unexpected error: %@ - Váratlan hiba: %@ - item status description + + Undelivered messages + Kézbesítetlen üzenetek + No comment provided by engineer. Unexpected migration state @@ -5875,8 +8102,8 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Unfav. - Nem kedvelt. - No comment provided by engineer. + Kedvenc megszüntetése + swipe action Unhide @@ -5913,6 +8140,11 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Ismeretlen hiba No comment provided by engineer. + + Unknown servers! + Ismeretlen kiszolgálók! + alert title + 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. @@ -5921,8 +8153,8 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej 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. - Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát. -A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. + 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. No comment provided by engineer. @@ -5932,7 +8164,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Unlink desktop? - Számítógép szétkapcsolása? + Leválasztja a számítógépet? No comment provided by engineer. @@ -5947,17 +8179,22 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Unmute - Némítás feloldása - No comment provided by engineer. + Némítás megszüntetése + notification label action Unread Olvasatlan + swipe action + + + 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 kerül elküldésre az új tagok számára. + Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára. No comment provided by engineer. @@ -5965,39 +8202,39 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Frissítés No comment provided by engineer. - - Update .onion hosts setting? - Tor .onion kiszolgálók beállításainak frissítése? - No comment provided by engineer. - Update database passphrase - Adatbázis jelmondat megváltoztatása + Az adatbázis jelmondatának módosítása No comment provided by engineer. Update network settings? - Hálózati beállítások megváltoztatása? + Módosítja a hálózati beállításokat? No comment provided by engineer. - - Update transport isolation mode? - Kapcsolat izolációs mód frissítése? + + Update settings? + Frissíti a beállításokat? + No comment provided by engineer. + + + Updated conditions + Frissített feltételek No comment provided by engineer. Updating settings will re-connect the client to all servers. - A beállítások frissítése a szerverekhez újra kapcsolódással jár. - No comment provided by engineer. - - - Updating this setting will re-connect the client to all servers. - A beállítás frissítésével a kliens újrakapcsolódik az összes kiszolgálóhoz. + 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 - A csevegés frissítése és megnyitása + 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. @@ -6010,24 +8247,54 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Fájl feltöltése server test step + + 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 %@ + %@ használata + No comment provided by engineer. + Use .onion hosts - Tor .onion kiszolgálók használata + Onion-kiszolgálók használata + No comment provided by engineer. + + + Use SOCKS proxy + SOCKS-proxy használata No comment provided by engineer. Use SimpleX Chat servers? - SimpleX Chat kiszolgálók használata? + SimpleX Chat-kiszolgálók 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 chat - Csevegés használata + SimpleX Chat használata No comment provided by engineer. @@ -6035,6 +8302,16 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol 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 @@ -6042,17 +8319,17 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Use from desktop - Használat számítógépről + Társítás számítógéppel No comment provided by engineer. Use iOS call interface - Az iOS hívófelület használata + Az iOS hívási felületét használata No comment provided by engineer. Use new incognito profile - Az új inkognító profil használata + Új inkognitóprofil használata No comment provided by engineer. @@ -6060,64 +8337,94 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol 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 the app while in the call. Használja az alkalmazást hívás közben. No comment provided by engineer. - - User profile - Felhasználói profil + + Use the app with one hand. + Használja az alkalmazást egy kézzel. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - A .onion kiszolgálók használatához kompatibilis VPN szolgáltatóra van szükség. + + 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. + SimpleX Chat-kiszolgálók használatban. No comment provided by engineer. Verify code with desktop - Kód ellenőrzése a számítógépen + Kód hitelesítése a számítógépen No comment provided by engineer. Verify connection - Kapcsolat ellenőrzése + Kapcsolat hitelesítése No comment provided by engineer. Verify connection security - Kapcsolat biztonságának ellenőrzése + Biztonságos kapcsolat hitelesítése No comment provided by engineer. Verify connections - Kapcsolatok ellenőrzése + Kapcsolatok hitelesítése No comment provided by engineer. Verify database passphrase - Adatbázis jelmondatának ellenőrzése + Az adatbázis jelmondatának hitelesítése No comment provided by engineer. Verify passphrase - Jelmondat ellenőrzése + Jelmondat hitelesítése No comment provided by engineer. Verify security code - Biztonsági kód ellenőrzése + Biztonsági kód hitelesítése No comment provided by engineer. @@ -6127,7 +8434,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Via secure quantum resistant protocol. - Biztonságos kvantum ellenálló protokoll által. + Biztonságos kvantumbiztos protokollon keresztül. No comment provided by engineer. @@ -6147,7 +8454,12 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Videos and files up to 1gb - Videók és fájlok 1Gb méretig + 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. @@ -6155,6 +8467,11 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Biztonsági kód megtekintése No comment provided by engineer. + + View updated conditions + Frissített feltételek megtekintése + No comment provided by engineer. + Visible history Látható előzmények @@ -6167,12 +8484,12 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Voice messages are prohibited in this chat. - A hangüzenetek le vannak tiltva ebben a csevegésben. + A hangüzenetek küldése le van tiltva ebben a csevegésben. No comment provided by engineer. - - Voice messages are prohibited in this group. - A hangüzenetek küldése le van tiltva ebben a csoportban. + + Voice messages are prohibited. + A hangüzenetek küldése le van tiltva. No comment provided by engineer. @@ -6182,7 +8499,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Voice messages prohibited! - A hangüzenetek le vannak tilva! + A hangüzenetek le vannak tiltva! No comment provided by engineer. @@ -6192,27 +8509,37 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Waiting for desktop... - Várakozás az asztali kliensre... + Várakozás a számítógép-alkalmazásra… No comment provided by engineer. Waiting for file - Fájlra várakozás + Várakozás a fájlra No comment provided by engineer. Waiting for image - Képre várakozás + Várakozás a képre No comment provided by engineer. Waiting for video - Videóra várakozás + 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. 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, továbbá üzenetkézbesítési hibákat okozhat + 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. @@ -6222,27 +8549,27 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol WebRTC ICE servers - WebRTC ICE kiszolgálók + WebRTC ICE-kiszolgálók No comment provided by engineer. Welcome %@! - Üdvözöllek %@! + Üdvözöljük %@! No comment provided by engineer. Welcome message - Üdvözlő üzenet + Üdvözlőüzenet No comment provided by engineer. Welcome message is too long - Az üdvözlő üzenet túl hosszú + Az üdvözlőüzenet túl hosszú No comment provided by engineer. What's new - Milyen újdonságok vannak + Újdonságok No comment provided by engineer. @@ -6255,9 +8582,9 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Amikor egy bejövő hang- vagy videóhívás érkezik. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Amikor az emberek kapcsolódást kérelmeznek, ön elfogadhatja vagy elutasíthatja azokat. + + 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. @@ -6282,37 +8609,52 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol With encrypted files and media. - Titkosított fájlokkal és médiatartalommal. + Titkosított fájlokkal és médiatartalmakkal. No comment provided by engineer. With optional welcome message. - Opcionális üdvözlő üzenettel. + Nem kötelező üdvözlőüzenettel. No comment provided by engineer. With reduced battery usage. - Csökkentett akkumulátorhasználattal. + 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. + No comment provided by engineer. + + + 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: %@. + alert message + Wrong database passphrase - Téves adatbázis jelmondat + Érvénytelen adatbázis-jelmondat No comment provided by engineer. + + 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. + snd 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. + file error text + Wrong passphrase! - Téves jelmondat! + Érvénytelen jelmondat! No comment provided by engineer. - - XFTP servers - XFTP kiszolgálók - No comment provided by engineer. - - - You - Ön + + XFTP server + XFTP-kiszolgáló No comment provided by engineer. @@ -6322,42 +8664,47 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol You accepted connection - Kapcsolódás elfogadva + Kapcsolat létrehozása No comment provided by engineer. You allow - Engedélyezte + Ö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ített névvel. Válasszon egy másik nevet. + 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 %@. - Már kapcsolódva van hozzá: %@. + Ö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 %@. - Már folyamatban van a kapcsolódás ehhez: %@. + 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álatos hivatkozáson keresztül! + 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 %@. - Már a(z) %@ csoport tagja. + Ö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) %@ csoporthoz. + A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz. No comment provided by engineer. @@ -6373,23 +8720,38 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol You are already joining the group! Repeat join request? - Csatlakozás folyamatban! -Csatlakozási kérés megismétlése? + A csatlakozás már folyamatban van a csoporthoz! +Megismé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. - Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. + Ö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 - Meghívást kapott a csoportba + Ö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 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áshitelesítés nélkül. + 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. @@ -6399,12 +8761,12 @@ Csatlakozási kérés megismétlése? You can enable later via Settings - Később engedélyezheti a Beállításokban + 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 alkalmazás Adatvédelem és biztonság menüpontban. + Később engedélyezheti őket az „Adatvédelem és biztonság” menüben. No comment provided by engineer. @@ -6414,47 +8776,57 @@ Csatlakozási kérés megismétlése? You can hide or mute a user profile - swipe it to the right. - Elrejthet vagy némíthat egy felhasználói profilt – csúsztasson jobbra. + 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 SimpleX ismerősök számára a Beállításokban. + Láthatóvá teheti a SimpleXbeli partnerei számára a „Beállításokban”. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Mostantól küldhet üzeneteket %@ számára notification body + + 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 beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét. + 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 csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait. + 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 az azonosítót az ismerőseivel, hogy kapcsolatba léphessenek önnel a **%@** nevű profilján keresztül. - No comment provided by engineer. - - - You can share your address as a link or QR code - anybody can connect to you. - Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz. + 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ü segítségével vagy az alkalmazás újraindításával indíthatja el + 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árolás a Beállításokon keresztül kapcsolható be. + A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be. No comment provided by engineer. @@ -6464,81 +8836,96 @@ Csatlakozási kérés megismétlése? You can view invitation link again in connection details. - A meghívó hivatkozást újra megtekintheti a kapcsolat részleteinél. - No comment provided by engineer. + A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. + alert message You can't send messages! Nem lehet üzeneteket küldeni! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Ön szabályozhatja, hogy mely kiszogál(ók)ón keresztül **kapja** az üzeneteket, az ismerősöket - az üzenetküldéshez használt szervereken. - No comment provided by engineer. - You could not be verified; please try again. - Nem lehetett ellenőrizni; próbálja meg újra. + 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ért egy kapcsolódási kérelmet ezen az azonosítón keresztül! + 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! Repeat connection request? - Már kért egy kapcsolódási kérelmet! -Kapcsolódási kérés megismétlése? - No comment provided by engineer. - - - You have no chats - Nincsenek csevegési üzenetek + Ön már küldött egy meghívási kérést! +Megismé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 kerül tárolásra. + 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 - Meghívta egy ismerősét + Ön meghívta egy partnerét No comment provided by engineer. You joined this group - Csatlakozott ehhez a csoporthoz + Ön csatlakozott ehhez a csoporthoz No comment provided by engineer. You joined this group. Connecting to inviting group member. - Csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. + Ön csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. + 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 ismerőstől. + 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. - Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősök számára. + 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 - Csoport meghívó elutasítva + Csoportmeghívó elutasítva No comment provided by engineer. You sent group invitation - Csoport meghívó elküldve + Csoportmeghívó elküldve No comment provided by engineer. + + You should receive notifications. + Ön megkapja az értesítéseket. + token info + 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! @@ -6546,22 +8933,22 @@ Kapcsolódási kérés megismétlése? You will be connected when group link host's device is online, please wait or check later! - Akkor lesz kapcsolódva, amikor a csoportos hivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! + 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 kapcsolódási kérelme elfogadásra kerül, várjon, vagy ellenőrizze később! + 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 le kapcsolódva, amikor az ismerőse eszköze online lesz, várjon, vagy ellenőrizze később! + 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 indításakor, vagy 30 másodpercnyi háttérben töltött idő után az alkalmazáshoz visszatérve hitelesítés szükséges. + 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. @@ -6574,6 +8961,11 @@ Kapcsolódási kérés megismétlése? 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. @@ -6581,42 +8973,27 @@ Kapcsolódási kérés megismétlése? You won't lose your contacts if you later delete your address. - Nem veszíti el az ismerőseit, ha később törli az azonosítóját. + Nem veszíti el a partnereit, ha később törli a címét. 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 ismerőst próbál meghívni, akivel inkognító profilt osztott meg abban a csoportban, amelyben saját fő profilja van használatban + 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 - Inkognító profilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében meghívók küldése tiltott - No comment provided by engineer. - - - Your %@ servers - %@ nevű profiljához tartozó kiszolgálók + 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 ICE servers - ICE kiszolgálók - No comment provided by engineer. - - - Your SMP servers - SMP kiszolgálók + Saját ICE-kiszolgálók No comment provided by engineer. Your SimpleX address - SimpleX azonosítója - No comment provided by engineer. - - - Your XFTP servers - XFTP kiszolgálók + Profil SimpleX-címe No comment provided by engineer. @@ -6634,36 +9011,44 @@ Kapcsolódási kérés megismétlése? A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz. No comment provided by engineer. + + Your chat preferences + Az Ön csevegési beállításai + alert title + Your chat profiles - Csevegési profiljai + Csevegési profilok No comment provided by engineer. - - 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). - Az ismerősnek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön. -Megszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással). + + 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 (%@). - Ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@). + 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. - Ismerősök engedélyezhetik a teljes üzenet törlést. + A partnerei engedélyezhetik a teljes üzenet törlését. No comment provided by engineer. Your contacts will remain connected. - Az ismerősei továbbra is kapcsolódva maradnak. + 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ÖRLŐDNI FOG, és a HELYÉRE az importált adatbázis kerül. + A jelenlegi csevegési adatbázis TÖRÖLVE és CSERÉLVE lesz az importáltra. No comment provided by engineer. @@ -6688,34 +9073,37 @@ Megszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt kés Your profile **%@** will be shared. - A(z) **%@** nevű profilja megosztásra fog kerülni. + 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. - Profilja az eszközön van tárolva, és csak az ismerősökkel kerül megosztásra. -A SimpleX kiszolgálók nem látjhatják profilját. + + 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. - - Your profile, contacts and delivered messages are stored on your device. - Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra. + + 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. No comment provided by engineer. + + 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. + alert message + Your random profile Véletlenszerű profil No comment provided by engineer. - - Your server - Saját kiszolgáló - No comment provided by engineer. - Your server address - Saját kiszolgáló cím + 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. @@ -6725,7 +9113,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Hozzájárulás](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. @@ -6735,7 +9123,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Csillag a GitHubon](https://github.com/simplex-chat/simplex-chat) + [Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. @@ -6755,22 +9143,27 @@ A SimpleX kiszolgálók nem látjhatják profilját. accepted call - elfogadott hívás + fogadott hívás call status + + accepted invitation + elfogadott meghívó + chat list item title + admin - admin + adminisztrátor member role admins - adminok + adminisztrátorok feature role agreeing encryption for %@… - titkosítás jóváhagyása %@ számára… + titkosítás elfogadása %@ számára… chat item text @@ -6780,7 +9173,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. all members - minden tag + összes tag feature role @@ -6790,7 +9183,17 @@ A SimpleX kiszolgálók nem látjhatják profilját. and %lld other events - és %lld további esemény + és további %lld esemény + No comment provided by engineer. + + + archived report + archivált jelentés + No comment provided by engineer. + + + attempts + próbálkozások No comment provided by engineer. @@ -6810,32 +9213,38 @@ A SimpleX kiszolgálók nem látjhatják profilját. bad message hash - téves üzenet hash + érvénytelen az üzenet hasítóértéke integrity error chat item blocked - blokkolva + letiltva marked deleted chat item preview text blocked %@ - %@ letiltva + letiltotta őt: %@ rcv group event chat item blocked by admin - letiltva az admin által - marked deleted chat item preview text + letiltva az adminisztrátor által + blocked chat item +marked deleted chat item preview text bold félkövér No comment provided by engineer. + + call + hívás + No comment provided by engineer. + call error - hiba a hívásban + híváshiba call status @@ -6850,22 +9259,22 @@ A SimpleX kiszolgálók nem látjhatják profilját. cancelled %@ - %@ törölve + %@ visszavonva feature offered item changed address for you - cím megváltoztatva + módosította a címet az Ön számára chat item text changed role of %1$@ to %2$@ - %1$@ szerepkörét megváltoztatta erre: %2$@ + a következőre módosította %1$@ szerepkörét: „%2$@” rcv group event chat item changed your role to %@ - megváltoztatta a szerepkörét erre: %@ + a következőre módosította az Ön szerepkörét: „%@” rcv group event chat item @@ -6875,12 +9284,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. changing address… - azonosító megváltoztatása… + cím módosítása… chat item text colored - színes + színezett No comment provided by engineer. @@ -6890,17 +9299,17 @@ A SimpleX kiszolgálók nem látjhatják profilját. connect to SimpleX Chat developers. - Kapcsolódás a SimpleX Chat fejlesztőkhöz. + kapcsolódás a SimpleX Chat fejlesztőkhöz. No comment provided by engineer. connected - kapcsolódva + kapcsolódott No comment provided by engineer. connected directly - közvetlenül kapcsolódva + közvetlenül kapcsolódott rcv group event chat item @@ -6920,7 +9329,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. connecting (introduced) - kapcsolódás (bejelentve) + kapcsolódás (bemutatkozva) No comment provided by engineer. @@ -6930,13 +9339,13 @@ A SimpleX kiszolgálók nem látjhatják profilját. connecting call… - hívás kapcsolódik… + kapcsolódási hívás… call status connecting… kapcsolódás… - chat list item title + No comment provided by engineer. connection established @@ -6950,32 +9359,32 @@ A SimpleX kiszolgálók nem látjhatják profilját. contact %1$@ changed to %2$@ - %1$@ megváltoztatta a nevét erre: %2$@ + %1$@ a következőre módosította a nevét: %2$@ profile update event chat item contact has e2e encryption - az ismerősnél az e2e titkosítás elérhető + a partner e2e titkosítással rendelkezik No comment provided by engineer. contact has no e2e encryption - az ismerősnél az e2e titkosítás nem elérhető + a partner nem rendelkezik e2e titkosítással No comment provided by engineer. creator - szerző + készítő No comment provided by engineer. custom - egyedi + egyéni dropdown time picker choice database version is newer than the app, but no down migration for: %@ - az adatbázis verziója újabb, mint az alkalmazásé, de nincs visszafelé átköltöztetés ehhez: %@ + 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. @@ -6983,10 +9392,16 @@ A SimpleX kiszolgálók nem látjhatják profilját. nap time unit + + decryption errors + visszafejtési hibák + No comment provided by engineer. + default (%@) alapértelmezett (%@) - pref value + delete after time +pref value default (no) @@ -7005,7 +9420,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. deleted contact - törölt ismerős + törölt partner rcv direct event chat item @@ -7015,7 +9430,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. different migration in the app/database: %@ / %@ - különböző átköltöztetések az alkalmazásban/adatbázisban: %@ / %@ + különböző átköltöztetés az alkalmazásban/adatbázisban: %@ / %@ No comment provided by engineer. @@ -7030,9 +9445,14 @@ A SimpleX kiszolgálók nem látjhatják profilját. duplicate message - duplikálódott üzenet + duplikált üzenet integrity error chat item + + duplicates + duplikációk + No comment provided by engineer. + e2e encrypted e2e titkosított @@ -7045,17 +9465,17 @@ A SimpleX kiszolgálók nem látjhatják profilját. enabled for contact - engedélyezve az ismerős számára + engedélyezve a partner számára enabled status enabled for you - engedélyezve az ön számára + engedélyezve az Ön számára enabled status encryption agreed - titkosítás egyeztetve + titkosítás elfogadva chat item text @@ -7070,27 +9490,27 @@ A SimpleX kiszolgálók nem látjhatják profilját. encryption ok for %@ - titkosítás rendben vele: %@ + titkosítás rendben %@ számára chat item text encryption re-negotiation allowed - titkosítás újraegyeztetés engedélyezve + a titkosítás újraegyeztetése engedélyezve van chat item text encryption re-negotiation allowed for %@ - titkosítás újraegyeztetés engedélyezve vele: %@ + a titkosítás újraegyeztetése engedélyezve van %@ számára chat item text encryption re-negotiation required - titkosítás újraegyeztetés szükséges + a titkosítás újraegyeztetése szükséges chat item text encryption re-negotiation required for %@ - titkosítás újraegyeztetés szükséges %@ számára + a titkosítás újraegyeztetése szükséges %@ számára chat item text @@ -7108,9 +9528,9 @@ A SimpleX kiszolgálók nem látjhatják profilját. hiba No comment provided by engineer. - - event happened - esemény történt + + expired + lejárt No comment provided by engineer. @@ -7125,7 +9545,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. group profile updated - csoport profil frissítve + csoportprofil frissítve snd group event chat item @@ -7135,27 +9555,32 @@ A SimpleX kiszolgálók nem látjhatják profilját. iOS Keychain is used to securely store passphrase - it allows receiving push notifications. - Az iOS kulcstár a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását. + 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 kulcstár az alkalmazás újraindítása, vagy a jelmondat módosítása után a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását. + 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. + + + inactive + inaktív No comment provided by engineer. incognito via contact address link - inkognitó a kapcsolattartási hivatkozáson keresztül + inkognitó a kapcsolattartási címhivatkozáson keresztül chat list item description incognito via group link - inkognitó a csoportos hivatkozáson keresztül + inkognitó a csoporthivatkozáson keresztül chat list item description incognito via one-time link - inkognitó az egyszer használatos hivatkozáson keresztül + inkognitó egy egyszer használható meghívón keresztül chat list item description @@ -7170,7 +9595,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. invalid chat data - érvénytelen csevegés adat + érvénytelen csevegésadat No comment provided by engineer. @@ -7183,24 +9608,29 @@ A SimpleX kiszolgálók nem látjhatják profilját. meghívás a(z) %@ csoportba group name + + invite + meghívás + No comment provided by engineer. + invited - meghívta + meghíva No comment provided by engineer. invited %@ - meghívta %@-t + meghívta őt: %@ rcv group event chat item invited to connect - meghívta, hogy csatlakozzon + Függőben lévő meghívó chat list item title invited via your group link - meghívta a csoport hivatkozásán keresztül + meghíva a saját csoporthivatkozásán keresztül rcv group event chat item @@ -7210,7 +9640,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. join as %@ - csatlakozás mint: %@ + csatlakozás mint %@ No comment provided by engineer. @@ -7220,7 +9650,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. marked deleted - töröltnek jelölve + törlésre jelölve marked deleted chat item preview text @@ -7230,7 +9660,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. member %1$@ changed to %2$@ - %1$@ megváltoztatta a nevét erre: %2$@ + %1$@ a következőre módosította a nevét: %2$@ profile update event chat item @@ -7238,6 +9668,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. kapcsolódott rcv group event chat item + + message + üzenet + No comment provided by engineer. + message received üzenet érkezett @@ -7263,6 +9698,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. moderálva lett %@ által marked deleted chat item preview text + + moderator + moderátor + member role + months hónap @@ -7271,7 +9711,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. never soha - No comment provided by engineer. + delete after time new message @@ -7300,10 +9740,10 @@ A SimpleX kiszolgálók nem látjhatják profilját. off - ki + kikapcsolva enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7312,14 +9752,24 @@ A SimpleX kiszolgálók nem látjhatják profilját. offered %1$@: %2$@ - ajánlotta %1$@: %2$@-kor + ajánlotta: %1$@, ekkor: %2$@ feature offered item on - be + bekapcsolva group pref value + + other + egyéb + No comment provided by engineer. + + + other errors + egyéb hibák + No comment provided by engineer. + owner tulajdonos @@ -7332,17 +9782,27 @@ A SimpleX kiszolgálók nem látjhatják profilját. peer-to-peer - ponttól-pontig + egyenrangú + 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. quantum resistant e2e encryption - végpontok közötti kvantumrezisztens titkosítás + végpontok közötti kvantumbiztos titkosítás chat item text received answer… - fogadott válasz… + válasz fogadása… No comment provided by engineer. @@ -7350,6 +9810,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. visszaigazolás fogadása… No comment provided by engineer. + + rejected + elutasítva + No comment provided by engineer. + rejected call elutasított hívás @@ -7362,24 +9827,29 @@ A SimpleX kiszolgálók nem látjhatják profilját. removed %@ - %@ eltávolítva + eltávolította őt: %@ rcv group event chat item removed contact address - törölt kapcsolattartási azonosító + eltávolította a kapcsolattartási címet profile update event chat item removed profile picture - törölt profilkép + eltávolította a profilképét profile update event chat item removed you - eltávolítottak + eltávolította Önt rcv group event chat item + + requested to connect + Függőben lévő meghívási kérelem + chat list item title + saved mentett @@ -7387,7 +9857,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. saved from %@ - mentve innen: %@ + elmentve innen: %@ + No comment provided by engineer. + + + search + keresés No comment provided by engineer. @@ -7407,7 +9882,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. security code changed - a biztonsági kód megváltozott + a biztonsági kód módosult chat item text @@ -7415,14 +9890,23 @@ A SimpleX kiszolgálók nem látjhatják profilját. közvetlen üzenet küldése No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + a kiszolgáló sorbaállítási információi: %1$@ + +utoljára fogadott üzenet: %2$@ + queue info + set new contact address - új kapcsolattartási azonosító beállítása + új kapcsolattartási címet állított be profile update event chat item set new profile picture - új profilkép beállítása + új profilképet állított be profile update event chat item @@ -7442,12 +9926,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. this contact - ez az ismerős + ez a partner notification title unblocked %@ - %@ feloldva + feloldotta %@ letiltását rcv group event chat item @@ -7455,14 +9939,24 @@ A SimpleX kiszolgálók nem látjhatják profilját. ismeretlen connection info + + unknown servers + ismeretlen átjátszók + No comment provided by engineer. + unknown status - ismeretlen státusz + ismeretlen állapot + No comment provided by engineer. + + + unprotected + nem védett No comment provided by engineer. updated group profile - módosított csoport profil + frissítette a csoport profilját rcv group event chat item @@ -7482,22 +9976,27 @@ A SimpleX kiszolgálók nem látjhatják profilját. via contact address link - kapcsolattartási azonosító-hivatkozáson keresztül + a kapcsolattartási címhivatkozáson keresztül chat list item description via group link - csoport hivatkozáson keresztül + a csoporthivatkozáson keresztül chat list item description via one-time link - egyszer használatos hivatkozáson keresztül + egy egyszer használható meghívón keresztül chat list item description via relay - átjátszón keresztül + egy továbbítókiszolgálón keresztül + No comment provided by engineer. + + + video + videó No comment provided by engineer. @@ -7507,7 +10006,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. waiting for answer… - várakozás válaszra… + várakozás a válaszra… No comment provided by engineer. @@ -7517,7 +10016,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. wants to connect to you! - kapcsolatba akar lépni önnel! + kapcsolatba akar lépni Önnel! No comment provided by engineer. @@ -7525,6 +10024,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. hét time unit + + when IP hidden + ha az IP-cím rejtett + No comment provided by engineer. + yes igen @@ -7532,72 +10036,72 @@ A SimpleX kiszolgálók nem látjhatják profilját. you - ön + Ön No comment provided by engineer. you are invited to group - meghívást kapott a csoportba + Ön meghívást kapott a csoportba No comment provided by engineer. you are observer - megfigyelő szerep + Ön megfigyelő No comment provided by engineer. you blocked %@ - blokkolta őt: %@ + Ön letiltotta őt: %@ snd group event chat item you changed address - azonosítója megváltoztatva + Ön módosította a címet chat item text you changed address for %@ - %@ azonosítója megváltoztatva + Ön módosította a címet %@ számára chat item text you changed role for yourself to %@ - saját szerepkör megváltoztatva erre: %@ + Ön a következőre módosította a saját szerepkörét: „%@” snd group event chat item you changed role of %1$@ to %2$@ - %1$@ szerepkörét megváltoztatta erre: %@ + Ön a következőre módosította %1$@ szerepkörét: „%2$@” snd group event chat item you left - elhagyta a csoportot + Ön elhagyta a csoportot snd group event chat item you removed %@ - eltávolította őt: %@ + Ön eltávolította őt: %@ snd group event chat item you shared one-time link - egyszer használatos hivatkozást osztott meg + Ön egy egyszer használható meghívót osztott meg chat list item description you shared one-time link incognito - egyszer használatos hivatkozást osztott meg inkognitóban + Ön egy egyszer használható meghívót osztott meg inkognitóban chat list item description you unblocked %@ - feloldotta %@ blokkolását + Ön feloldotta %@ letiltását snd group event chat item you: - ön: + Ön: No comment provided by engineer. @@ -7609,7 +10113,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
- +
@@ -7619,7 +10123,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - A SimpleX-nek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz. + A SimpleXnek 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 - Camera Usage Description @@ -7629,24 +10133,24 @@ A SimpleX kiszolgálók nem látjhatják profilját. SimpleX uses local network access to allow using user chat profile via desktop app on the same network. - A SimpleX helyi hálózati hozzáférést használ, hogy lehetővé tegye a felhasználói csevegőprofil használatát számítógépen keresztül ugyanazon a hálózaton. + 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 - Local Network Usage Description SimpleX needs microphone access for audio and video calls, and to record voice messages. - A SimpleX-nek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez. + 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 - Microphone Usage Description SimpleX needs access to Photo Library for saving captured and received media - A SimpleX-nek hozzáférésre van szüksége a Galériához a rögzített és fogadott média mentéséhez + A SimpleXnek galéria-hozzáférésre van szüksége a rögzített és fogadott média mentéséhez Privacy - Photo Library Additions Usage Description
- +
@@ -7666,4 +10170,250 @@ A SimpleX kiszolgálók nem látjhatják profilját.
+ +
+ +
+ + + %d new events + %d új esemény + notification body + + + From %d chat(s) + %d csevegésből + notification body + + + From: %@ + Tőle: %@ + notification body + + + New events + Új események + notification + + + New messages + Új üzenetek + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Minden jog fenntartva. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 index 124ddbcc33..9c675514f4 100644 --- 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 @@ -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/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 index cf485752ea..cb83427195 100644 --- 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 @@ -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/hu.xcloc/contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json index bc788c3c10..c07ec0f900 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "hu", "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/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index a5fe0ec830..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 @@
- +
- - - - - - 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) @@ -127,9 +100,14 @@ %@ è verificato/a No comment provided by engineer. + + %@ server + %@ server + No comment provided by engineer. + %@ servers - Server %@ + %@ server No comment provided by engineer. @@ -142,6 +120,11 @@ %@ si vuole connettere! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ e %lld membri @@ -162,11 +145,36 @@ %d giorni time interval + + %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. + forward confirmation reason + %d hours %d ore time interval + + %d messages not forwarded + %d messaggi non inoltrati + alert title + %d min %d min @@ -182,6 +190,11 @@ %d sec time interval + + %d seconds(s) + %d secondo/i + delete after time + %d skipped message(s) %d messaggio/i saltato/i @@ -252,11 +265,6 @@ %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 @@ -307,11 +315,6 @@ %u messaggi saltati. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nuovo) @@ -322,19 +325,9 @@ (questo dispositivo v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Aggiungi contatto**: per creare un nuovo link di invito o connetterti tramite un link che hai ricevuto. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link 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. @@ -342,13 +335,13 @@ **Crea gruppo**: per creare un nuovo gruppo. 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. + + **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). + + **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. @@ -362,11 +355,16 @@ **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. + + **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 / 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. @@ -392,11 +390,6 @@ \*grassetto* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +426,6 @@ - cronologia delle modifiche. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -451,7 +439,8 @@ 1 day 1 giorno - time interval + delete after time +time interval 1 hour @@ -466,12 +455,29 @@ 1 month 1 mese - time interval + delete after time +time interval 1 week 1 settimana - time interval + delete after time +time interval + + + 1 year + 1 anno + delete after time + + + 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 @@ -488,11 +494,6 @@ 30 secondi No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -542,31 +543,32 @@ Interrompere il cambio di indirizzo? No comment provided by engineer. - - About SimpleX - Riguardo SimpleX - No comment provided by engineer. - About SimpleX Chat Riguardo SimpleX Chat No comment provided by engineer. - - About SimpleX address - Info sull'indirizzo SimpleX + + About operators + Info sugli operatori No comment provided by engineer. - - Accent color - Colore principale + + Accent + Principale No comment provided by engineer. Accept Accetta accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Accetta le condizioni + No comment provided by engineer. Accept connection request? @@ -581,21 +583,47 @@ Accept incognito Accetta in incognito - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Condizioni accettate + No comment provided by engineer. + + + Acknowledged + Riconosciuto + No comment provided by engineer. + + + Acknowledgement errors + Errori di riconoscimento + No comment provided by engineer. + + + Active + Attivo + token status text + + + 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 contact - Aggiungi contatto + + Add friends + Aggiungi amici No comment provided by engineer. - - Add preset servers - Aggiungi server preimpostati + + Add list + Aggiungi elenco No comment provided by engineer. @@ -603,14 +631,19 @@ Aggiungi profilo No comment provided by engineer. + + 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 server… - Aggiungi server… + + Add team members + Aggiungi membri del team No comment provided by engineer. @@ -618,11 +651,46 @@ 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 @@ -633,6 +701,16 @@ 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 + No comment provided by engineer. + Admins can block a member for all. Gli amministratori possono bloccare un membro per tutti. @@ -648,6 +726,16 @@ Impostazioni di rete avanzate No comment provided by engineer. + + Advanced settings + Impostazioni avanzate + No comment provided by engineer. + + + All + Tutte + No comment provided by engineer. + All app data is deleted. Tutti i dati dell'app vengono eliminati. @@ -658,16 +746,31 @@ Tutte le chat e i messaggi verranno eliminati. Non è reversibile! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + Tutte le chat verranno rimosse dall'elenco %@ e l'elenco eliminato. + alert message + 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. 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! @@ -683,6 +786,21 @@ Tutti i nuovi messaggi da %@ verrranno nascosti! No comment provided by engineer. + + All profiles + Tutti gli profili + profile dropdown + + + 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. @@ -708,11 +826,21 @@ 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 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) @@ -738,11 +866,21 @@ Permetti l'invio di messaggi a tempo. No comment provided by engineer. + + 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 SimpleX links. Consenti di inviare link di SimpleX. @@ -808,6 +946,11 @@ Già in ingresso nel gruppo! 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 @@ -818,11 +961,21 @@ Viene creato un profilo di chat vuoto con il nome scelto e l'app si apre come al solito. No comment provided by engineer. + + Another reason + Altro motivo + report reason + 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: %@ @@ -838,6 +991,11 @@ 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 @@ -853,6 +1011,11 @@ 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 @@ -873,11 +1036,56 @@ 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? + No comment provided by engineer. + + + Archive reports + Archivia segnalazioni + swipe action + + + Archived contacts + Contatti archiviati + No comment provided by engineer. + Archiving database Archiviazione del database @@ -935,19 +1143,29 @@ Auto-accept contact requests - Auto-accetta richieste di contatto + Auto-accetta le richieste di contatto No comment provided by engineer. Auto-accept images - Auto-accetta immagini + Auto-accetta le immagini No comment provided by engineer. + + Auto-accept settings + Accetta automaticamente le impostazioni + alert title + Back Indietro No comment provided by engineer. + + Background + Sfondo + No comment provided by engineer. + Bad desktop address Indirizzo desktop errato @@ -963,16 +1181,61 @@ Hash 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 @@ -1008,6 +1271,16 @@ 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. + Both you and your contact can add message reactions. Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi. @@ -1038,11 +1311,35 @@ 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: +- send only legal content in public groups. +- respect other users – no spam. + Usando SimpleX Chat accetti di: +- inviare solo contenuto legale nei gruppi pubblici. +- rispettare gli altri utenti - niente spam. + No comment provided by engineer. + Call already ended! Chiamata già terminata! @@ -1053,11 +1350,26 @@ Chiamate No comment provided by engineer. + + 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! @@ -1068,10 +1380,16 @@ Impossibile invitare i contatti! No comment provided by engineer. + + Can't message member + Impossibile inviare un messaggio al membro + No comment provided by engineer. + Cancel Annulla - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1401,20 @@ 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. + Cannot receive file Impossibile ricevere il file - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Quota superata - il destinatario non ha ricevuto i messaggi precedentemente inviati. + snd error text Cellular @@ -1098,6 +1426,16 @@ Cambia No comment provided by engineer. + + Change automatic message deletion? + Cambiare l'eliminazione automatica dei messaggi? + alert title + + + Change chat profiles + Modifica profili utente + authentication reason + Change database passphrase? Cambiare password del database? @@ -1142,11 +1480,26 @@ Change self-destruct passcode Cambia codice di autodistruzione authentication reason - set passcode view +set passcode view - - 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. @@ -1164,6 +1517,11 @@ 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 @@ -1184,6 +1542,11 @@ 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! @@ -1194,15 +1557,50 @@ Preferenze della chat No comment provided by engineer. + + Chat preferences were changed. + Le preferenze della chat sono state cambiate. + alert message + + + 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. + No comment provided by engineer. + Check server address and try again. Controlla l'indirizzo del server e riprova. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1622,25 @@ 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 + No comment provided by engineer. + Clear Svuota - No comment provided by engineer. + swipe action Clear conversation @@ -1239,6 +1652,16 @@ 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? @@ -1249,11 +1672,21 @@ Annulla la verifica No comment provided by engineer. - - Colors - Colori + + 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. + + + Community guidelines violation + Violazione delle linee guida della comunità + report reason + Compare file Confronta file @@ -1264,11 +1697,56 @@ Confronta i codici di sicurezza con i tuoi contatti. 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 @@ -1279,11 +1757,21 @@ Conferma il codice di accesso 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 files from unknown servers. + Conferma i file da server sconosciuti. + No comment provided by engineer. + Confirm network settings Conferma le impostazioni di rete @@ -1309,6 +1797,11 @@ Conferma caricamento No comment provided by engineer. + + Confirmed + Confermato + token status text + Connect Connetti @@ -1329,6 +1822,11 @@ Connetti al desktop 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? @@ -1368,16 +1866,31 @@ Questo è il tuo link una tantum! Connettersi con %@ No comment provided by engineer. + + Connected + Connesso + No comment provided by engineer. + Connected desktop Desktop connesso 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 to server… Connessione al server… @@ -1388,6 +1901,11 @@ Questo è il tuo link una tantum! 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 @@ -1398,6 +1916,16 @@ Questo è il tuo link una tantum! 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 @@ -1408,11 +1936,38 @@ Questo è il tuo link una tantum! Errore di connessione (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + La connessione è bloccata dall'operatore del server: +%@ + 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 @@ -1423,6 +1978,16 @@ Questo è il tuo link una tantum! Connessione scaduta No comment provided by engineer. + + Connection with desktop stopped + Connessione con il desktop fermata + No comment provided by engineer. + + + Connections + Connessioni + No comment provided by engineer. + Contact allows Il contatto lo consente @@ -1433,6 +1998,11 @@ Questo è il tuo link una tantum! Il contatto esiste già No comment provided by engineer. + + Contact deleted! + Contatto eliminato! + No comment provided by engineer. + Contact hidden: Contatto nascosto: @@ -1443,9 +2013,9 @@ Questo è il tuo link una tantum! Il contatto è connesso notification - - Contact is not connected yet! - Il contatto non è ancora connesso! + + Contact is deleted. + Il contatto è stato eliminato. No comment provided by engineer. @@ -1458,6 +2028,11 @@ Questo è il tuo link una tantum! 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 @@ -1468,21 +2043,41 @@ Questo è il tuo link una tantum! I contatti possono contrassegnare i messaggi per l'eliminazione; potrai vederli. No comment provided by engineer. + + Content violates conditions of use + Il contenuto viola le condizioni di utilizzo + blocking reason + Continue Continua No comment provided by engineer. + + Conversation deleted! + Conversazione eliminata! + No comment provided by engineer. + Copy Copia - chat item action + 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 %@? @@ -1493,6 +2088,11 @@ Questo è il tuo link una tantum! Crea No comment provided by engineer. + + Create 1-time link + Crea link una tantum + No comment provided by engineer. + Create SimpleX address Crea indirizzo SimpleX @@ -1503,11 +2103,6 @@ Questo è il tuo link una tantum! Crea un gruppo usando un profilo casuale. 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. - No comment provided by engineer. - Create file Crea file @@ -1528,6 +2123,11 @@ Questo è il tuo link una tantum! Crea link No comment provided by engineer. + + Create list + Crea elenco + 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/). 💻 @@ -1553,6 +2153,11 @@ Questo è il tuo link una tantum! Crea il tuo profilo No comment provided by engineer. + + Created + Creato + No comment provided by engineer. + Created at Creato il @@ -1563,11 +2168,6 @@ Questo è il tuo link una tantum! Creato il: %@ copied message info - - Created on %@ - Creato il %@ - No comment provided by engineer. - Creating archive link Creazione link dell'archivio @@ -1583,11 +2183,21 @@ Questo è il tuo link una tantum! Codice di accesso attuale 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 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 %@. @@ -1598,11 +2208,26 @@ Questo è il tuo link una tantum! 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 ID ID database @@ -1701,6 +2326,11 @@ Questo è il tuo link una tantum! Il database verrà migrato al riavvio dell'app No comment provided by engineer. + + Debug delivery + Debug della consegna + No comment provided by engineer. + Decentralized Decentralizzato @@ -1714,18 +2344,19 @@ Questo è il tuo link una tantum! Delete Elimina - chat item action + alert action +swipe action + + + Delete %lld messages of members? + Eliminare %lld messaggi dei membri? + No comment provided by engineer. Delete %lld messages? Eliminare %lld messaggi? No comment provided by engineer. - - Delete Contact - Elimina contatto - No comment provided by engineer. - Delete address Elimina indirizzo @@ -1751,14 +2382,14 @@ Questo è il tuo link una tantum! 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. @@ -1771,6 +2402,11 @@ Questo è il tuo link una tantum! Eliminare il profilo di chat? No comment provided by engineer. + + Delete chat? + Eliminare la chat? + No comment provided by engineer. + Delete connection Elimina connessione @@ -1781,11 +2417,9 @@ Questo è il tuo link una tantum! Elimina contatto No comment provided by engineer. - - Delete contact? -This cannot be undone! - Eliminare il contatto? -Non è reversibile! + + Delete contact? + Eliminare il contatto? No comment provided by engineer. @@ -1848,6 +2482,11 @@ Non è reversibile! Eliminare il link? No comment provided by engineer. + + Delete list? + Eliminare l'elenco? + alert title + Delete member message? Eliminare il messaggio del membro? @@ -1861,7 +2500,7 @@ Non è reversibile! Delete messages Elimina messaggi - No comment provided by engineer. + alert button Delete messages after @@ -1878,9 +2517,9 @@ Non è reversibile! 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. @@ -1898,11 +2537,31 @@ Non è reversibile! Elimina coda server test step + + 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 + No comment provided by engineer. + + + Deleted + Eliminato + No comment provided by engineer. + Deleted at Eliminato il @@ -1913,6 +2572,16 @@ Non è reversibile! Eliminato il: %@ copied message info + + 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 @@ -1948,11 +2617,41 @@ Non è reversibile! 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 %@. + No comment provided by engineer. + + + Destination server error: %@ + Errore del server di destinazione: %@ + snd error text + + + 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 @@ -1983,8 +2682,13 @@ Non è reversibile! Messaggi diretti chat feature - - Direct messages between members are prohibited in this group. + + 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. @@ -1998,11 +2702,26 @@ Non è reversibile! Disattiva SimpleX Lock authentication reason + + Disable automatic message deletion? + Disattivare l'eliminazione automatica dei messaggi? + alert title + + + Disable delete messages + Disattiva eliminazione messaggi + alert button + Disable for all Disattiva per tutti No comment provided by engineer. + + Disabled + Disattivato + No comment provided by engineer. + Disappearing message Messaggio a tempo @@ -2018,8 +2737,8 @@ Non è reversibile! I messaggi a tempo sono vietati in questa chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. I messaggi a tempo sono vietati in questo gruppo. No comment provided by engineer. @@ -2053,11 +2772,21 @@ Non è reversibile! Individua via rete locale 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 SimpleX for emergency calls. NON usare SimpleX per chiamate di emergenza. No comment provided by engineer. + + Do NOT use private routing. + NON usare l'instradamento privato. + No comment provided by engineer. + Do it later Fallo dopo @@ -2068,6 +2797,16 @@ Non è reversibile! Non inviare la cronologia ai nuovi membri. No comment provided by engineer. + + Do not use credentials with proxy. + Non usare credenziali con proxy. + No comment provided by engineer. + + + Documents: + Documenti: + No comment provided by engineer. + Don't create address Non creare un indirizzo @@ -2078,11 +2817,21 @@ Non è reversibile! 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 @@ -2091,7 +2840,13 @@ Non è reversibile! Download Scarica - chat item action + alert button +chat item action + + + Download errors + Errori di scaricamento + No comment provided by engineer. Download failed @@ -2103,6 +2858,21 @@ Non è reversibile! Scarica file server test step + + Download files + Scarica i file + alert action + + + Downloaded + Scaricato + No comment provided by engineer. + + + Downloaded files + File scaricati + No comment provided by engineer. + Downloading archive Scaricamento archivio @@ -2123,6 +2893,11 @@ Non è reversibile! Durata No comment provided by engineer. + + E2E encrypted notifications. + Notifiche crittografate E2E. + No comment provided by engineer. + Edit Modifica @@ -2143,6 +2918,11 @@ Non è reversibile! Attiva (mantieni sostituzioni) 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 SimpleX Lock Attiva SimpleX Lock @@ -2156,7 +2936,7 @@ Non è reversibile! Enable automatic message deletion? Attivare l'eliminazione automatica dei messaggi? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2983,11 @@ Non è reversibile! Attiva il codice di autodistruzione set passcode view + + Enabled + Attivato + No comment provided by engineer. + Enabled for Attivo per @@ -2278,6 +3063,11 @@ Non è reversibile! Rinegoziazione crittografia fallita. No comment provided by engineer. + + Encryption renegotiation in progress. + Rinegoziazione della crittografia in corso. + No comment provided by engineer. + Enter Passcode Inserisci il codice di accesso @@ -2343,26 +3133,36 @@ Non è reversibile! Errore nell'interruzione del cambio di indirizzo No comment provided by engineer. + + Error accepting conditions + Errore di accettazione delle condizioni + alert title + 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 No comment provided by engineer. + + Error adding server + Errore di aggiunta del server + alert title + 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 @@ -2373,6 +3173,21 @@ Non è reversibile! 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 @@ -2388,6 +3203,11 @@ Non è reversibile! Errore nella creazione del link del gruppo No comment provided by engineer. + + Error creating list + Errore nella creazione dell'elenco + alert title + Error creating member contact Errore di creazione del contatto @@ -2403,6 +3223,11 @@ Non è reversibile! 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 @@ -2423,11 +3248,6 @@ Non è reversibile! 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 @@ -2473,6 +3293,11 @@ Non è reversibile! 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 @@ -2483,9 +3308,14 @@ Non è reversibile! Errore di ingresso nel gruppo No comment provided by engineer. - - Error loading %@ servers - Errore nel caricamento dei server %@ + + Error loading servers + Errore nel caricamento dei server + alert title + + + Error migrating settings + Errore nella migrazione delle impostazioni No comment provided by engineer. @@ -2496,16 +3326,36 @@ Non è reversibile! Error receiving file Errore nella ricezione del file + alert title + + + Error reconnecting server + Errore di riconnessione al server No comment provided by engineer. + + Error reconnecting servers + Errore di riconnessione ai server + No comment provided by engineer. + + + Error registering for notifications + Errore di registrazione per le notifiche + alert title + Error removing member Errore nella rimozione del membro No comment provided by engineer. - - Error saving %@ servers - Errore nel salvataggio dei server %@ + + Error reordering lists + Errore riordinando gli elenchi + alert title + + + Error resetting statistics + Errore di azzeramento statistiche No comment provided by engineer. @@ -2513,6 +3363,11 @@ Non è reversibile! Errore nel salvataggio dei server ICE No comment provided by engineer. + + Error saving chat list + Errore nel salvataggio dell'elenco di chat + alert title + Error saving group profile Errore nel salvataggio del profilo del gruppo @@ -2528,6 +3383,11 @@ Non è reversibile! Errore nel salvataggio della password nel portachiavi No comment provided by engineer. + + Error saving servers + Errore di salvataggio dei server + alert title + Error saving settings Errore di salvataggio delle impostazioni @@ -2573,16 +3433,26 @@ Non è reversibile! Errore nell'interruzione della chat No comment provided by engineer. + + Error switching profile + Errore nel cambio di profilo + No comment provided by engineer. + Error switching profile! Errore nel cambio di profilo! - No comment provided by engineer. + alertTitle 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 @@ -2593,6 +3463,11 @@ Non è reversibile! Errore nell'aggiornamento del messaggio No comment provided by engineer. + + Error updating server + Errore di aggiornamento del server + alert title + Error updating settings Errore nell'aggiornamento delle impostazioni @@ -2621,7 +3496,9 @@ Non è reversibile! Error: %@ Errore: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3510,16 @@ Non è reversibile! Errore: nessun file di database No comment provided by engineer. + + Errors + Errori + No comment provided by engineer. + + + Errors in servers configuration. + Errori nella configurazione dei server. + servers error + Even when disabled in the conversation. Anche quando disattivato nella conversazione. @@ -2648,6 +3535,11 @@ Non è reversibile! Espandi chat item action + + Expired + Scaduto + token status text + Export database Esporta database @@ -2658,6 +3550,11 @@ Non è reversibile! Errore di esportazione: No comment provided by engineer. + + Export theme + Esporta tema + No comment provided by engineer. + Exported database archive. Archivio database esportato. @@ -2683,16 +3580,70 @@ Non è reversibile! 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. + No comment provided by engineer. + Favorite Preferito + swipe action + + + Favorites + Preferite No comment provided by engineer. + + File error + Errore del file + file error alert title + + + File errors: +%@ + Errori di file: +%@ + alert message + + + File is blocked by server operator: +%@. + Il file è bloccato dall'operatore del server: +%@. + 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: %@ + file error text + + + File status + Stato del file + No comment provided by engineer. + + + File status: %@ + Stato del file: %@ + copied message info + File will be deleted from servers. Il file verrà eliminato dai server. @@ -2713,6 +3664,11 @@ Non è reversibile! File: %@ No comment provided by engineer. + + Files + File + No comment provided by engineer. + Files & media File e multimediali @@ -2723,8 +3679,8 @@ Non è reversibile! File e multimediali chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. File e contenuti multimediali sono vietati in questo gruppo. No comment provided by engineer. @@ -2793,21 +3749,71 @@ Non è reversibile! Correzione non supportata dal membro del gruppo No comment provided by engineer. + + For all moderators + Per tutti i moderatori + No comment provided by engineer. + + + For chat profile %@: + Per il profilo di chat %@: + servers error + 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 + No comment provided by engineer. + Forward Inoltra chat item action + + Forward %d message(s)? + Inoltrare %d messaggio/i? + alert title + Forward and save messages Inoltra e salva i messaggi No comment provided by engineer. + + Forward messages + Inoltra i messaggi + alert action + + + Forward messages without files? + Inoltrare i messaggi senza file? + alert message + + + Forward up to 20 messages at once. + Inoltra fino a 20 messaggi alla volta. + No comment provided by engineer. + Forwarded Inoltrato @@ -2818,6 +3824,40 @@ Non è reversibile! 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Server di inoltro: %1$@ +Errore del server di destinazione: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Server di inoltro: %1$@ +Errore: %2$@ + snd error text + Found desktop Desktop trovato @@ -2838,11 +3878,6 @@ Non è reversibile! 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. @@ -2863,6 +3898,21 @@ Non è reversibile! GIF e adesivi No comment provided by engineer. + + Get notified when mentioned. + Ricevi una notifica quando menzionato. + No comment provided by engineer. + + + Good afternoon! + Buon pomeriggio! + message preview + + + Good morning! + Buongiorno! + message preview + Group Gruppo @@ -2918,41 +3968,6 @@ Non è reversibile! 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. (24 hours) - I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore) - No comment provided by engineer. - - - Group members can send SimpleX links. - I membri del gruppo possono inviare link di Simplex. - 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. - No comment provided by engineer. - Group message: Messaggio del gruppo: @@ -2993,11 +4008,21 @@ Non è reversibile! 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 @@ -3048,10 +4073,20 @@ Non è reversibile! Come funziona SimpleX 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 + No comment provided by engineer. + How it works Come funziona - No comment provided by engineer. + alert button How to @@ -3078,6 +4113,11 @@ Non è reversibile! Server ICE (uno per riga) No comment provided by engineer. + + IP address + Indirizzo IP + 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. @@ -3118,8 +4158,8 @@ Non è reversibile! Immediatamente No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Immune a spam e abusi No comment provided by engineer. @@ -3143,11 +4183,23 @@ Non è reversibile! 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. +More improvements are coming soon! + Consegna migliorata, utilizzo di traffico ridotto. +Altri miglioramenti sono in arrivo! + No comment provided by engineer. + Improved message delivery Consegna dei messaggi migliorata @@ -3178,6 +4230,16 @@ Non è reversibile! Suoni nelle chiamate No comment provided by engineer. + + Inappropriate content + Contenuto inappropriato + report reason + + + Inappropriate profile + Profilo inappropriato + report reason + Incognito Incognito @@ -3248,6 +4310,11 @@ Non è reversibile! 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! @@ -3255,16 +4322,41 @@ Non è reversibile! No comment provided by engineer. - - Instantly - Istantaneamente - No comment provided by engineer. - Interface Interfaccia No comment provided by engineer. + + Interface colors + Colori dell'interfaccia + No comment provided by engineer. + + + 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) + token status text + Invalid QR code Codice QR non valido @@ -3303,7 +4395,7 @@ Non è reversibile! Invalid server address! Indirizzo del server non valido! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4417,11 @@ Non è reversibile! Invita membri No comment provided by engineer. + + Invite to chat + Invita in chat + No comment provided by engineer. + Invite to group Invita al gruppo @@ -3340,8 +4437,8 @@ Non è reversibile! L'eliminazione irreversibile dei messaggi è vietata in questa chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. L'eliminazione irreversibile dei messaggi è vietata in questo gruppo. No comment provided by engineer. @@ -3366,6 +4463,11 @@ Non è reversibile! 3. 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 (%@). @@ -3384,7 +4486,7 @@ Non è reversibile! Join Entra - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4528,11 @@ Questo è il tuo link per il gruppo %@! Keep Tieni + alert action + + + Keep conversation + Tieni la conversazione No comment provided by engineer. @@ -3436,7 +4543,7 @@ Questo è il tuo link per il gruppo %@! Keep unused invitation? Tenere l'invito inutilizzato? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4578,16 @@ Questo è il tuo link per il gruppo %@! Leave Esci + swipe action + + + Leave chat + Esci dalla chat + No comment provided by engineer. + + + Leave chat? + Uscire dalla chat? No comment provided by engineer. @@ -3513,6 +4630,21 @@ Questo è il tuo link per il gruppo %@! Desktop collegati No comment provided by engineer. + + List + Elenco + swipe action + + + 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 message! Messaggio in diretta! @@ -3523,11 +4655,6 @@ Questo è il tuo link per il gruppo %@! Messaggi in diretta No comment provided by engineer. - - Local - Locale - No comment provided by engineer. - Local name Nome locale @@ -3548,11 +4675,6 @@ Questo è il tuo link per il gruppo %@! 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 @@ -3563,21 +4685,11 @@ Questo è il tuo link per il gruppo %@! 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 @@ -3603,11 +4715,36 @@ Questo è il tuo link per il gruppo %@! Max 30 secondi, ricevuto istantaneamente. No comment provided by engineer. + + Media & file servers + Server di multimediali e file + No comment provided by engineer. + + + Medium + Media + blur media + Member Membro No comment provided by engineer. + + Member inactive + Membro inattivo + item status text + + + Member reports + Segnalazioni dei membri + chat feature + + + 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. @@ -3618,11 +4755,66 @@ Questo è il tuo link per il gruppo %@! 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 SimpleX links. + I membri del gruppo possono inviare link di Simplex. + 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 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 delivery error Errore di recapito del messaggio @@ -3633,9 +4825,29 @@ Questo è il tuo link per il gruppo %@! Ricevute di consegna dei messaggi! No comment provided by engineer. + + Message delivery warning + Avviso di consegna del messaggio + item status text + Message draft - Bozza dei messaggi + Bozza del messaggio + No comment provided by engineer. + + + Message forwarded + Messaggio inoltrato + item status text + + + Message may be delivered later if member becomes active. + Il messaggio può essere consegnato più tardi se il membro diventa attivo. + item status description + + + Message queue info + Info coda messaggi No comment provided by engineer. @@ -3648,16 +4860,41 @@ Questo è il tuo link per il gruppo %@! Le reazioni ai messaggi sono vietate in questa chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Le reazioni ai messaggi sono vietate in questo gruppo. 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 + No comment provided by engineer. + + + Message status: %@ + Stato del messaggio: %@ + copied message info + Message text Testo del messaggio @@ -3683,6 +4920,26 @@ Questo è il tuo link per il gruppo %@! I messaggi da %@ verranno mostrati! No comment provided by engineer. + + Messages in this chat will never be deleted. + I messaggi in questa chat non verranno mai eliminati. + alert message + + + Messages received + Messaggi ricevuti + No comment provided by engineer. + + + Messages sent + Messaggi inviati + No comment provided by engineer. + + + Messages were deleted after you selected them. + I messaggi sono stati eliminati dopo che li hai selezionati. + alert message + 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. @@ -3748,9 +5005,9 @@ Questo è il tuo link per il gruppo %@! La migrazione è completata No comment provided by engineer. - - Migrations: %@ - Migrazioni: %@ + + Migrations: + Migrazioni: No comment provided by engineer. @@ -3768,6 +5025,11 @@ Questo è il tuo link per il gruppo %@! Moderato il: %@ copied message info + + More + Altro + swipe action + More improvements are coming soon! Altri miglioramenti sono in arrivo! @@ -3778,16 +5040,16 @@ Questo è il tuo link per il gruppo %@! Connessione di rete più affidabile. No comment provided by engineer. + + More reliable notifications + Notifiche più affidabili + No comment provided by engineer. + Most likely this connection is deleted. Probabilmente questa connessione è stata eliminata. item status description - - 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 @@ -3796,7 +5058,12 @@ Questo è il tuo link per il gruppo %@! Mute Silenzia - No comment provided by engineer. + notification label action + + + Mute all + Silenzia tutto + notification label action Muted when inactive! @@ -3806,7 +5073,7 @@ Questo è il tuo link per il gruppo %@! Name Nome - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +5085,26 @@ Questo è il tuo link per il gruppo %@! Connessione di rete No comment provided by engineer. + + Network decentralization + Decentralizzazione della rete + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo. + snd error text + 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 @@ -3833,16 +5115,36 @@ Questo è il tuo link per il gruppo %@! Stato della rete No comment provided by engineer. + + New + Nuovo + token status text + New Passcode Nuovo codice di accesso 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. + No comment provided by engineer. + New chat Nuova chat No comment provided by engineer. + + New chat experience 🎉 + Una nuova esperienza di chat 🎉 + No comment provided by engineer. + New contact request Nuova richiesta di contatto @@ -3853,11 +5155,6 @@ Questo è il tuo link per il gruppo %@! Nuovo contatto: notification - - New database archive - Nuovo archivio database - No comment provided by engineer. - New desktop app! Nuova app desktop! @@ -3868,11 +5165,21 @@ Questo è il tuo link per il gruppo %@! Nuovo nome da mostrare No comment provided by engineer. + + New events + Nuovi eventi + notification + 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 @@ -3888,6 +5195,11 @@ Questo è il tuo link per il gruppo %@! Nuova password… No comment provided by engineer. + + New server + Nuovo server + No comment provided by engineer. + No No @@ -3898,6 +5210,21 @@ Questo è il tuo link per il gruppo %@! Nessuna password dell'app Authentication unavailable + + 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 @@ -3918,6 +5245,11 @@ Questo è il tuo link per il gruppo %@! Nessun token del dispositivo! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Ancora nessuna connessione diretta, il messaggio viene inoltrato dall'amministratore. + item status description + No filtered chats Nessuna chat filtrata @@ -3933,26 +5265,111 @@ Questo è il tuo link per il gruppo %@! Nessuna cronologia No comment provided by engineer. + + No info, try to reload + Nessuna informazione, prova a ricaricare + No comment provided by engineer. + + + No media & file servers. + Nessun server di multimediali e file. + servers error + + + No message + Nessun messaggio + No comment provided by engineer. + + + No message servers. + Nessun server dei messaggi. + servers error + 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 No comment provided by engineer. + + 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. + servers error + + + No token! + Nessun token! + alert title + + + 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 + No comment provided by engineer. + + + Nothing to forward! + Niente da inoltrare! + alert title + Notifications Notifiche @@ -3963,6 +5380,21 @@ Questo è il tuo link per il gruppo %@! Le notifiche sono disattivate! No comment provided by engineer. + + Notifications error + Errore delle notifiche + alert title + + + Notifications privacy + Privacy delle notifiche + No comment provided by engineer. + + + Notifications status + Stato delle notifiche + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5412,35 @@ Questo è il tuo link per il gruppo %@! Off Off - No comment provided by engineer. + blur media Ok Ok - No comment provided by engineer. + alert button Old database Database vecchio No comment provided by engineer. - - Old database archive - Vecchio archivio del database - No comment provided by engineer. - 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. +Requires compatible VPN. + Gli host Onion saranno **necessari** per la connessione. +Richiede 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. +Requires compatible VPN. + Gli host Onion verranno usati quando disponibili. +Richiede l'attivazione della VPN. No comment provided by engineer. @@ -4017,11 +5448,21 @@ Questo è il tuo link per il gruppo %@! 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**. + + 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. @@ -4037,6 +5478,16 @@ Questo è il tuo link per il gruppo %@! 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. @@ -4090,13 +5541,18 @@ Questo è il tuo link per il gruppo %@! Open Apri - No comment provided by engineer. + alert action Open Settings Apri le impostazioni No comment provided by engineer. + + Open changes + Apri le modifiche + No comment provided by engineer. + Open chat Apri chat @@ -4107,31 +5563,45 @@ Questo è il tuo link per il gruppo %@! Apri la console della chat authentication reason + + Open conditions + Apri le condizioni + No comment provided by engineer. + Open group Apri gruppo No comment provided by engineer. + + Open link? + alert title + Open migration to another device Apri migrazione ad un altro dispositivo authentication reason - - Open user profiles - Apri i profili utente - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Protocollo e codice open source: chiunque può gestire i server. - No comment provided by engineer. - Opening app… Apertura dell'app… No comment provided by engineer. + + Operator + Operatore + No comment provided by engineer. + + + Operator server + Server dell'operatore + alert title + + + Or import archive file + O importa file archivio + No comment provided by engineer. + Or paste archive link O incolla il link dell'archivio @@ -4152,11 +5622,28 @@ Questo è il tuo link per il gruppo %@! 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 file errors: +%@ + Altri errori di file: +%@ + alert message + PING count Conteggio PING @@ -4192,6 +5679,11 @@ Questo è il tuo link per il gruppo %@! Codice di accesso impostato! No comment provided by engineer. + + Password + Password + No comment provided by engineer. + Password to show Password per mostrare @@ -4222,13 +5714,13 @@ Questo è il tuo link per il gruppo %@! Incolla il link che hai ricevuto 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 + + Periodic Periodicamente No comment provided by engineer. @@ -4242,11 +5734,28 @@ Questo è il tuo link per il gruppo %@! Chiamate picture-in-picture 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. +Please 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. +Si 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. @@ -4314,11 +5823,31 @@ Errore: %@ Conserva la password in modo sicuro, NON potrai cambiarla se la perdi. No comment provided by engineer. + + 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. + token info + Polish interface Interfaccia polacca No comment provided by engineer. + + Port + Porta + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata @@ -4329,41 +5858,86 @@ Errore: %@ Conserva la bozza dell'ultimo messaggio, con gli allegati. No comment provided by engineer. - - Preset server - Server preimpostato - No comment provided by engineer. - Preset server address Indirizzo server preimpostato No comment provided by engineer. + + 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 🚀 + No comment provided by engineer. + Private notes Note private name of notes to self + + 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 @@ -4379,25 +5953,20 @@ Errore: %@ Immagini del profilo No comment provided by engineer. - - Profile name - Nome del profilo - No comment provided by engineer. - - - Profile name: - Nome del profilo: - No comment provided by engineer. - Profile password Password del profilo No comment provided by engineer. + + Profile theme + Tema del profilo + No comment provided by engineer. + Profile update will be sent to your contacts. L'aggiornamento del profilo verrà inviato ai tuoi contatti. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,6 +5988,11 @@ Errore: %@ 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 SimpleX links. Vieta l'invio di link di SimpleX. @@ -4444,11 +6018,23 @@ Errore: %@ Proibisci l'invio di messaggi vocali. No comment provided by engineer. + + Protect IP address + Proteggi l'indirizzo IP + No comment provided by engineer. + Protect app screen Proteggi la schermata dell'app No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Proteggi il tuo indirizzo IP dai relay di messaggistica scelti dai tuoi contatti. +Attivalo nelle impostazioni *Rete e server*. + No comment provided by engineer. + Protect your chat profiles with a password! Proteggi i tuoi profili di chat con una password! @@ -4464,6 +6050,21 @@ Errore: %@ 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 @@ -4484,6 +6085,11 @@ Errore: %@ Valuta l'app No comment provided by engineer. + + Reachable chat toolbar + Barra degli strumenti di chat accessibile + No comment provided by engineer. + React… Reagisci… @@ -4492,33 +6098,28 @@ Errore: %@ Read Leggi - No comment provided by engineer. + swipe action 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). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Maggiori informazioni nel nostro repository GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Maggiori informazioni nel nostro [repository GitHub](https://github.com/simplex-chat/simplex-chat#readme). @@ -4529,6 +6130,11 @@ Errore: %@ Le ricevute sono disattivate No comment provided by engineer. + + Receive errors + Errori di ricezione + No comment provided by engineer. + Received at Ricevuto il @@ -4549,16 +6155,26 @@ Errore: %@ Messaggio ricevuto message info title + + 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. No comment provided by engineer. - - Receiving concurrency - Ricezione concomitanza - No comment provided by engineer. - Receiving file will be stopped. La ricezione del file verrà interrotta. @@ -4584,11 +6200,36 @@ Errore: %@ 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? @@ -4609,10 +6250,26 @@ Errore: %@ Consumo di batteria ridotto No comment provided by engineer. + + Register + Registra + No comment provided by engineer. + + + Register notification token? + Registrare il token di notifica? + token info + + + Registered + Registrato + token status text + Reject Rifiuta - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4639,6 +6296,16 @@ Errore: %@ 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 @@ -4699,6 +6366,56 @@ Errore: %@ Rispondi chat item action + + Report + Segnala + chat item action + + + 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. + report reason + + + Report reason? + Motivo della segnalazione? + No comment provided by engineer. + + + 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 reason + + + Report: %@ + Segnalazione: %@ + report in notification + + + Reporting messages to moderators is prohibited. + È vietato segnalare messaggi ai moderatori. + No comment provided by engineer. + + + Reports + Segnalazioni + No comment provided by engineer. + Required Obbligatorio @@ -4709,16 +6426,41 @@ Errore: %@ 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 @@ -4759,9 +6501,9 @@ Errore: %@ Rivela chat item action - - Revert - Ripristina + + Review conditions + Leggi le condizioni No comment provided by engineer. @@ -4789,11 +6531,21 @@ Errore: %@ Avvia chat No comment provided by engineer. - - SMP servers + + SMP server Server SMP No comment provided by engineer. + + SOCKS proxy + Proxy SOCKS + No comment provided by engineer. + + + Safely receive files + Ricevi i file in sicurezza + No comment provided by engineer. + Safer groups Gruppi più sicuri @@ -4802,43 +6554,44 @@ Errore: %@ Save Salva - chat item action + alert button +chat item action 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. + alert button 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 @@ -4852,7 +6605,7 @@ Errore: %@ Save preferences? Salvare le preferenze? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6620,18 @@ Errore: %@ Save servers? Salvare i server? - No comment provided by engineer. - - - Save settings? - Salvare le impostazioni? - No comment provided by engineer. + alert title Save welcome message? Salvare il messaggio di benvenuto? No comment provided by engineer. + + Save your profile? + Salvare il profilo? + alert title + Saved Salvato @@ -4899,6 +6652,21 @@ Errore: %@ Messaggio salvato message info title + + 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 QR code Scansiona codice QR @@ -4939,11 +6707,21 @@ Errore: %@ Cerca o incolla un link SimpleX No comment provided by engineer. + + Secondary + Secondario + No comment provided by engineer. + Secure queue Coda sicura server test step + + Secured + Protetto + No comment provided by engineer. + Security assessment Valutazione della sicurezza @@ -4957,6 +6735,21 @@ Errore: %@ Select Seleziona + chat item action + + + 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. @@ -4994,11 +6787,6 @@ Errore: %@ Invia ricevute di consegna a 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 @@ -5009,9 +6797,14 @@ Errore: %@ Invia messaggio a tempo No comment provided by engineer. + + Send errors + Errori di invio + No comment provided by engineer. + Send link previews - Invia anteprime dei link + Invia le anteprime dei link No comment provided by engineer. @@ -5019,14 +6812,29 @@ Errore: %@ 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. @@ -5052,7 +6860,7 @@ Errore: %@ Sender cancelled file transfer. Il mittente ha annullato il trasferimento del file. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6917,11 @@ Errore: %@ Inviato il: %@ copied message info + + Sent directly + Inviato direttamente + No comment provided by engineer. + Sent file event Evento file inviato @@ -5119,11 +6932,71 @@ Errore: %@ Messaggio inviato message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + Server aggiunto all'operatore %@. + alert message + + + Server address + Indirizzo server + No comment provided by engineer. + + + Server address is incompatible with network settings. + L'indirizzo del server non è compatibile con le impostazioni di rete. + srv error text. + + + Server address is incompatible with network settings: %@. + L'indirizzo del server è incompatibile con le impostazioni di rete: %@. + No comment provided by engineer. + + + Server operator changed. + L'operatore del server è cambiato. + alert title + + + Server operators + Operatori server + No comment provided by engineer. + + + Server protocol changed. + Il protocollo del server è cambiato. + alert title + Server requires authorization to create queues, check password Il server richiede l'autorizzazione di creare code, controlla la password @@ -5139,11 +7012,36 @@ Errore: %@ Test del server fallito! No comment provided by engineer. + + Server type + Tipo server + No comment provided by engineer. + + + Server version is incompatible with network settings. + La versione del server non è compatibile con le impostazioni di rete. + srv error text + + + 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 @@ -5154,11 +7052,21 @@ Errore: %@ 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 @@ -5169,6 +7077,11 @@ Errore: %@ Impostalo al posto dell'autenticazione di sistema. No comment provided by engineer. + + Set message expiration in chats. + Imposta la scadenza dei messaggi nelle chat. + No comment provided by engineer. + Set passcode Imposta codice @@ -5199,6 +7112,11 @@ Errore: %@ Impostazioni No comment provided by engineer. + + Settings were changed. + Le impostazioni sono state cambiate. + alert message + Shape profile images Forma delle immagini del profilo @@ -5207,21 +7125,42 @@ Errore: %@ Share Condividi - chat item action + alert action +chat item action 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 SimpleX address on social media. + Condividi l'indirizzo SimpleX sui social media. + No comment provided by engineer. + Share address Condividi indirizzo No comment provided by engineer. + + Share address publicly + Condividi indirizzo pubblicamente + No comment provided by engineer. + Share address with contacts? Condividere l'indirizzo con i contatti? + alert title + + + Share from other apps. + Condividi da altre app. No comment provided by engineer. @@ -5229,16 +7168,31 @@ Errore: %@ Condividi link No comment provided by engineer. + + Share profile + Condividi il profilo + 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 QR code Mostra codice QR @@ -5259,21 +7213,46 @@ Errore: %@ 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 → on messages sent via private routing. + Mostra → nei messaggi inviati via instradamento privato. + 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 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. @@ -5304,6 +7283,21 @@ Errore: %@ 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? + No comment provided by engineer. + + + SimpleX channel link + Link del canale SimpleX + simplex link type + SimpleX contact address Indirizzo di contatto SimpleX @@ -5324,8 +7318,8 @@ Errore: %@ Link di SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. I link di SimpleX sono vietati in questo gruppo. No comment provided by engineer. @@ -5339,11 +7333,21 @@ Errore: %@ Invito SimpleX una tantum simplex link type + + 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 @@ -5359,16 +7363,49 @@ Errore: %@ Piccoli gruppi (max 20) No comment provided by engineer. + + Soft + Leggera + blur media + + + 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + Alcuni server hanno fallito il test: +%@ + alert message + Somebody Qualcuno notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Quadrata, circolare o qualsiasi forma tra le due. @@ -5389,6 +7426,16 @@ Errore: %@ Avvia la migrazione No comment provided by engineer. + + Starting from %@. + Inizio da %@. + No comment provided by engineer. + + + Statistics + Statistiche + No comment provided by engineer. + Stop Ferma @@ -5404,11 +7451,6 @@ Errore: %@ Ferma la chat No comment provided by engineer. - - Stop chat to enable database actions - Ferma la chat per attivare le azioni del database - 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. @@ -5437,28 +7479,63 @@ Errore: %@ Stop sharing Smetti di condividere - No comment provided by engineer. + alert action Stop sharing address? Smettere di condividere l'indirizzo? - No comment provided by engineer. + alert title Stopping chat Arresto della chat No comment provided by engineer. + + Storage + Archiviazione + No comment provided by engineer. + + + Strong + Forte + blur media + 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 @@ -5469,11 +7546,21 @@ Errore: %@ Autenticazione di sistema 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 @@ -5489,11 +7576,21 @@ Errore: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Coda + No comment provided by engineer. + Take picture Scatta foto 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 button Tocca il pulsante @@ -5529,16 +7626,21 @@ Errore: %@ Tocca per scansionare No comment provided by engineer. - - Tap to start a new chat - Tocca per iniziare una chat - No comment provided by engineer. + + Temporary file error + Errore del file temporaneo + file error alert title Test failed at step %@. Test fallito al passo %@. server test failure + + Test notifications + Prova le notifiche + No comment provided by engineer. + Test server Prova server @@ -5552,7 +7654,7 @@ Errore: %@ Tests failed! Test falliti! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7671,6 @@ Errore: %@ 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 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. @@ -5586,6 +7683,16 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.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. @@ -5596,6 +7703,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il codice che hai scansionato non è un codice QR di link SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, 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! @@ -5616,6 +7728,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.La crittografia funziona e il nuovo accordo sulla crittografia non è richiesto. Potrebbero verificarsi errori di connessione! No comment provided by engineer. + + The future of messaging + La nuova generazione di messaggistica privata + No comment provided by engineer. + The hash of the previous message is different. L'hash del messaggio precedente è diverso. @@ -5631,9 +7748,14 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.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. @@ -5641,9 +7763,14 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato. No comment provided by engineer. - - The profile is only shared with your contacts. - Il profilo è condiviso solo con i tuoi contatti. + + 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. @@ -5661,14 +7788,29 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.I server per le nuove connessioni del profilo di chat attuale **%@**. No comment provided by engineer. + + 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. + The text you pasted is not a SimpleX link. Il testo che hai incollato non è un link SimpleX. No comment provided by engineer. - - Theme - Tema + + 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. @@ -5691,6 +7833,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questa azione non può essere annullata: i messaggi inviati e ricevuti prima di quanto selezionato verranno eliminati. Potrebbe richiedere diversi minuti. No comment provided by engineer. + + 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. + alert message + 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. @@ -5736,11 +7883,31 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questo è il tuo link una tantum! 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: @@ -5761,9 +7928,9 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.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. @@ -5771,6 +7938,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Per proteggere il fuso orario, i file immagine/vocali usano UTC. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,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à. 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. @@ -5788,26 +7980,61 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio 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. No comment provided by engineer. + + Token status: %@. + Stato del token: %@. + token status + + + 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: %@). @@ -5863,10 +8090,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Sbloccare il membro? No comment provided by engineer. - - Unexpected error: %@ - Errore imprevisto: % @ - item status description + + Undelivered messages + Messaggi non consegnati + No comment provided by engineer. Unexpected migration state @@ -5876,7 +8103,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Unfav. Non pref. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +8140,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Errore sconosciuto No comment provided by engineer. + + Unknown servers! + Server sconosciuti! + alert title + 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. @@ -5948,11 +8180,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Unmute Riattiva notifiche - No comment provided by engineer. + notification label action Unread Non letto + swipe action + + + Unsupported connection link + Link di connessione non supportato No comment provided by engineer. @@ -5965,11 +8202,6 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Aggiorna No comment provided by engineer. - - Update .onion hosts setting? - Aggiornare l'impostazione degli host .onion? - No comment provided by engineer. - Update database passphrase Aggiorna la password del database @@ -5980,9 +8212,14 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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 No comment provided by engineer. @@ -5990,16 +8227,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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. - No comment provided by engineer. - Upgrade and open chat Aggiorna e apri chat No comment provided by engineer. + + Upload errors + Errori di invio + No comment provided by engineer. + Upload failed Invio fallito @@ -6010,21 +8247,51 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Invia file server test step + + 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 %@ + Usa %@ + No comment provided by engineer. + Use .onion hosts Usa gli host .onion No comment provided by engineer. + + Use SOCKS proxy + Usa proxy SOCKS + No comment provided by engineer. + Use SimpleX Chat servers? Usare i server di SimpleX Chat? No comment provided by engineer. + + 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 chat Usa la chat @@ -6035,6 +8302,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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 @@ -6060,24 +8337,54 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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 the app while in the call. Usa l'app mentre sei in chiamata. No comment provided by engineer. - - User profile - Profilo utente + + Use the app with one hand. + Usa l'app con una mano sola. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - L'uso di host .onion richiede un fornitore di VPN compatibile. + + 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. @@ -6150,11 +8457,21 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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 + No comment provided by engineer. + Visible history Cronologia visibile @@ -6170,8 +8487,8 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e I messaggi vocali sono vietati in questa chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. I messaggi vocali sono vietati in questo gruppo. No comment provided by engineer. @@ -6210,6 +8527,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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. + 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 @@ -6255,9 +8582,9 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Quando si connettono le chiamate audio e video. 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 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. @@ -6295,26 +8622,41 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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. + No comment provided by engineer. + + + 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: %@. + alert message + Wrong database passphrase Password del database sbagliata No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata. + snd 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. + file error text + Wrong passphrase! Password sbagliata! No comment provided by engineer. - - XFTP servers + + XFTP server Server XFTP No comment provided by engineer. - - 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. @@ -6340,6 +8682,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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 %@. @@ -6387,11 +8734,26 @@ Ripetere la richiesta di ingresso? 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 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 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 @@ -6422,11 +8784,21 @@ Ripetere la richiesta di ingresso? Puoi renderlo visibile ai tuoi contatti SimpleX nelle impostazioni. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Ora puoi inviare messaggi a %@ notification body + + 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. @@ -6442,16 +8814,16 @@ Ripetere la richiesta di ingresso? 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. - 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 No comment provided by engineer. + + 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. @@ -6465,23 +8837,23 @@ Ripetere la richiesta di ingresso? 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. + alert message You can't send messages! Non puoi inviare messaggi! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Tu decidi attraverso quale/i server **ricevere** i messaggi, i tuoi contatti quali server usi per inviare loro i messaggi. - No comment provided by engineer. - You could not be verified; please try again. Non è stato possibile verificarti, riprova. No comment provided by engineer. + + You decide who can connect. + Sei tu a decidere chi può connettersi. + No comment provided by engineer. + You have already requested connection via this address! Hai già richiesto la connessione tramite questo indirizzo! @@ -6494,11 +8866,6 @@ Repeat connection request? Ripetere 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. @@ -6519,11 +8886,26 @@ Ripetere la richiesta di connessione? Sei entrato/a in questo gruppo. Connessione al membro del gruppo invitante. 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. @@ -6539,6 +8921,11 @@ Ripetere la richiesta di connessione? Hai inviato un invito al gruppo No comment provided by engineer. + + You should receive notifications. + Dovresti ricevere le notifiche. + token info + 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! @@ -6574,6 +8961,11 @@ Ripetere la richiesta di connessione? Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi. No comment provided by engineer. + + You will stop receiving messages from this 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. @@ -6594,31 +8986,16 @@ Ripetere la richiesta di connessione? 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 ICE servers I tuoi server ICE No comment provided by engineer. - - Your SMP servers - I tuoi server SMP - No comment provided by engineer. - Your SimpleX address Il tuo indirizzo SimpleX No comment provided by engineer. - - Your XFTP servers - I tuoi server XFTP - No comment provided by engineer. - Your calls Le tue chiamate @@ -6634,16 +9011,19 @@ Ripetere la richiesta di connessione? Il tuo database della chat non è crittografato: imposta la password per crittografarlo. No comment provided by engineer. + + Your chat preferences + Le tue preferenze della chat + alert title + Your chat profiles I tuoi profili di chat No comment provided by engineer. - - 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). - 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). + + 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. @@ -6661,6 +9041,11 @@ Puoi annullare questa connessione e rimuovere il contatto (e riprovare più tard I tuoi contatti resteranno connessi. No comment provided by engineer. + + 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. @@ -6691,33 +9076,36 @@ Puoi annullare questa connessione e rimuovere il contatto (e riprovare più tard Verrà condiviso il tuo profilo **%@**. No comment provided by engineer. - - 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. + + 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. - - 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. + + 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. No comment provided by engineer. + + 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. + alert message + Your random profile Il tuo profilo casuale No comment provided by engineer. - - Your server - Il tuo server - No comment provided by engineer. - Your server address L'indirizzo del tuo server No comment provided by engineer. + + Your servers + I tuoi server + No comment provided by engineer. + Your settings Le tue impostazioni @@ -6758,6 +9146,11 @@ I server di SimpleX non possono vedere il tuo profilo. chiamata accettata call status + + accepted invitation + invito accettato + chat list item title + admin amministratore @@ -6793,6 +9186,16 @@ I server di SimpleX non possono vedere il tuo profilo. e altri %lld eventi No comment provided by engineer. + + archived report + segnalazione archiviata + No comment provided by engineer. + + + attempts + tentativi + No comment provided by engineer. + audio call (not e2e encrypted) chiamata audio (non crittografata e2e) @@ -6826,13 +9229,19 @@ I server di SimpleX non possono vedere il tuo profilo. blocked by admin bloccato dall'amministratore - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold grassetto No comment provided by engineer. + + call + chiama + No comment provided by engineer. + call error errore di chiamata @@ -6865,7 +9274,7 @@ I server di SimpleX non possono vedere il tuo profilo. changed your role to %@ - cambiato il tuo ruolo in %@ + ha cambiato il tuo ruolo in %@ rcv group event chat item @@ -6936,7 +9345,7 @@ I server di SimpleX non possono vedere il tuo profilo. connecting… in connessione… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +9392,16 @@ I server di SimpleX non possono vedere il tuo profilo. giorni time unit + + decryption errors + errori di decifrazione + No comment provided by engineer. + default (%@) predefinito (%@) - pref value + delete after time +pref value default (no) @@ -7033,6 +9448,11 @@ I server di SimpleX non possono vedere il tuo profilo. messaggio duplicato integrity error chat item + + duplicates + doppi + No comment provided by engineer. + e2e encrypted crittografato e2e @@ -7108,9 +9528,9 @@ I server di SimpleX non possono vedere il tuo profilo. errore No comment provided by engineer. - - event happened - evento accaduto + + expired + scaduto No comment provided by engineer. @@ -7143,6 +9563,11 @@ I server di SimpleX non possono vedere il tuo profilo. 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. + + inactive + inattivo + No comment provided by engineer. + incognito via contact address link incognito via link indirizzo del contatto @@ -7183,6 +9608,11 @@ I server di SimpleX non possono vedere il tuo profilo. invito al gruppo %@ group name + + invite + invita + No comment provided by engineer. + invited ha invitato @@ -7230,7 +9660,7 @@ I server di SimpleX non possono vedere il tuo profilo. member %1$@ changed to %2$@ - membro %1$@ cambiato in %2$@ + il membro %1$@ è diventato %2$@ profile update event chat item @@ -7238,6 +9668,11 @@ I server di SimpleX non possono vedere il tuo profilo. si è connesso/a rcv group event chat item + + message + messaggio + No comment provided by engineer. + message received messaggio ricevuto @@ -7263,6 +9698,11 @@ I server di SimpleX non possono vedere il tuo profilo. moderato da %@ marked deleted chat item preview text + + moderator + moderatore + member role + months mesi @@ -7271,7 +9711,7 @@ I server di SimpleX non possono vedere il tuo profilo. never mai - No comment provided by engineer. + delete after time new message @@ -7302,8 +9742,8 @@ I server di SimpleX non possono vedere il tuo profilo. off off enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9760,16 @@ I server di SimpleX non possono vedere il tuo profilo. on group pref value + + other + altro + No comment provided by engineer. + + + other errors + altri errori + No comment provided by engineer. + owner proprietario @@ -7335,6 +9785,16 @@ I server di SimpleX non possono vedere il tuo profilo. peer-to-peer No comment provided by engineer. + + pending + in attesa + No comment provided by engineer. + + + pending approval + in attesa di approvazione + No comment provided by engineer. + quantum resistant e2e encryption crittografia e2e resistente alla quantistica @@ -7350,6 +9810,11 @@ I server di SimpleX non possono vedere il tuo profilo. conferma ricevuta… No comment provided by engineer. + + rejected + rifiutato + No comment provided by engineer. + rejected call chiamata rifiutata @@ -7380,6 +9845,11 @@ I server di SimpleX non possono vedere il tuo profilo. ti ha rimosso/a rcv group event chat item + + requested to connect + richiesto di connettersi + chat list item title + saved salvato @@ -7390,6 +9860,11 @@ I server di SimpleX non possono vedere il tuo profilo. salvato da %@ No comment provided by engineer. + + search + cerca + No comment provided by engineer. + sec sec @@ -7415,6 +9890,15 @@ I server di SimpleX non possono vedere il tuo profilo. invia messaggio diretto No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + info coda server: %1$@ + +ultimo msg ricevuto: %2$@ + queue info + set new contact address impostato nuovo indirizzo di contatto @@ -7422,7 +9906,7 @@ I server di SimpleX non possono vedere il tuo profilo. set new profile picture - impostata nuova immagine del profilo + ha impostato una nuova immagine del profilo profile update event chat item @@ -7455,11 +9939,21 @@ I server di SimpleX non possono vedere il tuo profilo. sconosciuto connection info + + unknown servers + relay sconosciuti + No comment provided by engineer. + unknown status stato sconosciuto No comment provided by engineer. + + unprotected + non protetto + No comment provided by engineer. + updated group profile ha aggiornato il profilo del gruppo @@ -7500,6 +9994,11 @@ I server di SimpleX non possono vedere il tuo profilo. via relay No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) videochiamata (non crittografata e2e) @@ -7525,6 +10024,11 @@ I server di SimpleX non possono vedere il tuo profilo. settimane time unit + + when IP hidden + quando l'IP è nascosto + No comment provided by engineer. + yes @@ -7609,7 +10113,7 @@ I server di SimpleX non possono vedere il tuo profilo.
- +
@@ -7646,7 +10150,7 @@ I server di SimpleX non possono vedere il tuo profilo.
- +
@@ -7666,4 +10170,250 @@ I server di SimpleX non possono vedere il tuo profilo.
+ +
+ +
+ + + %d new events + %d nuovi eventi + notification body + + + From %d chat(s) + Da %d chat + notification body + + + From: %@ + Da: %@ + notification body + + + New events + Nuovi eventi + notification + + + New messages + Nuovi messaggi + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Tutti i diritti riservati. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 3f32998707..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 @@
- +
- - - - - - 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) (コピー可能) @@ -74,7 +47,7 @@ %@ (current) - %@ (現在) + %@ (現在) No comment provided by engineer. @@ -127,6 +100,11 @@ %@ は検証されています No comment provided by engineer. + + %@ server + %@ サーバー + No comment provided by engineer. + %@ servers %@ サーバー @@ -142,6 +120,11 @@ %@ が接続を希望しています! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@や%@など%lld人のメンバー @@ -162,11 +145,36 @@ %d 日 time interval + + %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 個のファイルがダウンロードされていません。 + forward confirmation reason + %d hours %d 時 time interval + + %d messages not forwarded + %d 個のメッセージが未転送 + alert title + %d min %d 分 @@ -182,6 +190,10 @@ %d 秒 time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d 件のスキップされたメッセージ @@ -194,7 +206,7 @@ %lld - %lld + No comment provided by engineer. @@ -252,11 +264,6 @@ %lldつの新しいインターフェース言語 No comment provided by engineer. - - %lld second(s) - %lld 秒 - No comment provided by engineer. - %lld seconds %lld 秒 @@ -307,11 +314,6 @@ %u 件のメッセージがスキップされました。 No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (新規) @@ -322,33 +324,23 @@ (このデバイス v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。 No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **新しい連絡先を追加**: 連絡先のワンタイム QR コードまたはリンクを作成します。 - No comment provided by engineer. - **Create group**: to create a new group. **グループ作成**: 新しいグループを作成する。 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. + + **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). + + **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. @@ -362,11 +354,16 @@ **注意**: パスフレーズを紛失すると、パスフレーズを復元または変更できなくなります。 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. + + **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 / Paste link**: to connect via a link you received. + **QRスキャン / リンクの貼り付け**: 受け取ったリンクで接続する。 + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。 @@ -392,15 +389,13 @@ \*太字* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). - faster and more stable. + - [ディレクトリサービス](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) に接続 (ベータ)! +- 配信証明を送信する (最大 20 人まで)。 +- より速く、より安定。 No comment provided by engineer. @@ -416,6 +411,9 @@ - optionally notify deleted contacts. - profile names with spaces. - and more! + - 任意で削除された連絡先へ通知します。 +- プロフィール名に空白を含めることができます。 +- and more! No comment provided by engineer. @@ -427,11 +425,6 @@ - 編集履歴。 No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 秒 @@ -445,7 +438,8 @@ 1 day 1日 - time interval + delete after time +time interval 1 hour @@ -460,12 +454,28 @@ 1 month 1ヶ月 - time interval + delete after time +time interval 1 week 1週間 - time interval + delete after time +time interval + + + 1 year + delete after time + + + 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 @@ -482,11 +492,6 @@ 30秒 No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -536,31 +541,29 @@ アドレス変更を中止しますか? No comment provided by engineer. - - About SimpleX - SimpleXについて - No comment provided by engineer. - About SimpleX Chat SimpleX Chat について No comment provided by engineer. - - About SimpleX address - SimpleXアドレスについて + + About operators No comment provided by engineer. - - Accent color - アクセントカラー + + Accent No comment provided by engineer. Accept 承諾 accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + No comment provided by engineer. Accept connection request? @@ -575,20 +578,40 @@ Accept incognito シークレットモードで承諾 - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + No comment provided by engineer. + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active + token status text + + + 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 contact + + Add friends No comment provided by engineer. - - Add preset servers - 既存サーバを追加 + + Add list No comment provided by engineer. @@ -596,14 +619,18 @@ プロフィールを追加 No comment provided by engineer. + + Add server + サーバを追加 + No comment provided by engineer. + Add servers by scanning QR codes. QRコードでサーバを追加する。 No comment provided by engineer. - - Add server… - サーバを追加… + + Add team members No comment provided by engineer. @@ -611,11 +638,41 @@ 別の端末に追加 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 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address アドレス @@ -626,6 +683,14 @@ アドレス変更は中止されます。古い受信アドレスが使用されます。 No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. No comment provided by engineer. @@ -640,6 +705,15 @@ ネットワーク詳細設定 No comment provided by engineer. + + Advanced settings + 詳細設定 + No comment provided by engineer. + + + All + No comment provided by engineer. + All app data is deleted. すべてのアプリデータが削除されます。 @@ -650,16 +724,28 @@ 全チャットとメッセージが削除されます(※元に戻せません※)! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + alert message + 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. グループ全員の接続が継続します。 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. @@ -673,6 +759,19 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + すべてのプロフィール + profile dropdown + + + 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. あなたの連絡先が繋がったまま継続します。 @@ -698,14 +797,22 @@ 連絡先が通話を許可している場合のみ通話を許可する。 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 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. @@ -728,11 +835,20 @@ 消えるメッセージの送信を許可する。 No comment provided by engineer. + + 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 SimpleX links. SimpleXリンクの送信を許可。 @@ -798,6 +914,11 @@ すでにグループに参加しています! No comment provided by engineer. + + Always use private routing. + プライベートルーティングを常に使用する。 + No comment provided by engineer. + Always use relay 常にリレーを経由する @@ -808,11 +929,20 @@ 指定された名前の空のチャット プロファイルが作成され、アプリが通常どおり開きます。 No comment provided by engineer. + + Another reason + report reason + Answer call 通話に応答 No comment provided by engineer. + + Anybody can host servers. + プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。 + No comment provided by engineer. + App build: %@ アプリのビルド: %@ @@ -828,6 +958,10 @@ アプリは新しいローカルファイル(ビデオを除く)を暗号化します。 No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon アプリのアイコン @@ -843,6 +977,10 @@ アプリのパスコードは自己破壊パスコードに置き換えられます。 No comment provided by engineer. + + App session + No comment provided by engineer. + App version アプリのバージョン @@ -855,15 +993,54 @@ 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? + 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? + No comment provided by engineer. + + + Archive reports + swipe action + + + Archived contacts No comment provided by engineer. @@ -930,11 +1107,19 @@ 画像を自動的に受信 No comment provided by engineer. + + Auto-accept settings + alert title + Back 戻る No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -949,15 +1134,51 @@ メッセージのハッシュ値問題 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. @@ -986,6 +1207,14 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. 自分も相手もメッセージへのリアクションを追加できます。 @@ -1016,11 +1245,29 @@ ブルガリア語、フィンランド語、タイ語、ウクライナ語 - ユーザーと [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: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! 通話は既に終了してます! @@ -1031,10 +1278,22 @@ 通話 No comment provided by engineer. + + 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! 連絡先を招待できません! @@ -1045,10 +1304,15 @@ 連絡先を招待できません! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel 中止 - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1059,10 +1323,18 @@ データベースのパスワードを保存するためのキーチェーンにアクセスできません No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file ファイル受信ができません - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text Cellular @@ -1073,6 +1345,14 @@ 変更 No comment provided by engineer. + + Change automatic message deletion? + alert title + + + Change chat profiles + authentication reason + Change database passphrase? データベースのパスフレーズを更新しますか? @@ -1117,11 +1397,22 @@ Change self-destruct passcode 自己破壊パスコードを変更する authentication reason - set passcode view +set passcode view - - 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. @@ -1139,6 +1430,10 @@ チャットのデータベースが削除されました No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported チャットのデータベースが読み込まれました @@ -1158,6 +1453,10 @@ 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. @@ -1167,15 +1466,45 @@ チャット設定 No comment provided by engineer. + + Chat preferences were changed. + alert message + + + 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. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. サーバのアドレスを確認してから再度試してください。 - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1196,10 +1525,24 @@ ライブラリから選択 No comment provided by engineer. + + Chunks deleted + チャンクが削除されました + No comment provided by engineer. + + + Chunks downloaded + チャンクがダウンロードされました + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear 消す - No comment provided by engineer. + swipe action Clear conversation @@ -1211,8 +1554,17 @@ ダイアログのクリアしますか? 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. @@ -1220,11 +1572,19 @@ 検証を消す No comment provided by engineer. - - Colors - + + Color chats with the new themes. No comment provided by engineer. + + Color mode + 色設定 + No comment provided by engineer. + + + Community guidelines violation + report reason + Compare file ファイルを比較 @@ -1235,11 +1595,48 @@ 連絡先とセキュリティコードを確認する。 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 確認 @@ -1250,11 +1647,19 @@ パスコードを確認 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. @@ -1277,6 +1682,10 @@ Confirm upload No comment provided by engineer. + + Confirmed + token status text + Connect 接続 @@ -1293,6 +1702,12 @@ Connect to desktop + デスクトップに接続 + No comment provided by engineer. + + + Connect to your friends faster. + 友達ともっと速くつながりましょう。 No comment provided by engineer. @@ -1327,12 +1742,29 @@ This is your own one-time link! Connect with %@ 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. @@ -1345,8 +1777,14 @@ This is your own one-time link! サーバーに接続中… (エラー: %@) 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. @@ -1354,6 +1792,15 @@ This is your own one-time link! 接続 No comment provided by engineer. + + Connection and servers status. + 接続とサーバーのステータス。 + No comment provided by engineer. + + + Connection blocked + No comment provided by engineer. + Connection error 接続エラー @@ -1364,13 +1811,35 @@ This is your own one-time link! 接続エラー (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + 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. @@ -1378,6 +1847,14 @@ This is your own one-time link! 接続タイムアウト No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows 連絡先の許可 @@ -1388,6 +1865,10 @@ This is your own one-time link! 連絡先に既に存在します No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: 連絡先が非表示: @@ -1398,9 +1879,8 @@ This is your own one-time link! 連絡先は接続中 notification - - Contact is not connected yet! - 連絡先がまだ繋がってません! + + Contact is deleted. No comment provided by engineer. @@ -1413,6 +1893,10 @@ This is your own one-time link! 連絡先の設定 No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts 連絡先 @@ -1423,21 +1907,37 @@ This is your own one-time link! 連絡先はメッセージを削除対象とすることができます。あなたには閲覧可能です。 No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue 続ける No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy コピー - chat item action + 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. @@ -1447,6 +1947,10 @@ This is your own one-time link! 作成 No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address SimpleXアドレスの作成 @@ -1456,11 +1960,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - 人とつながるためのアドレスを作成する。 - No comment provided by engineer. - Create file ファイルを作成 @@ -1480,6 +1979,10 @@ This is your own one-time 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/)で新しいプロファイルを作成します。 💻 @@ -1487,6 +1990,7 @@ This is your own one-time link! Create profile + プロフィールを作成する No comment provided by engineer. @@ -1504,6 +2008,10 @@ This is your own one-time link! プロフィールを作成する No comment provided by engineer. + + Created + No comment provided by engineer. + Created at No comment provided by engineer. @@ -1512,11 +2020,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - %@ によって作成されました - No comment provided by engineer. - Creating archive link No comment provided by engineer. @@ -1530,11 +2033,19 @@ This is your own one-time link! 現在のパスコード 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 passphrase… 現在の暗証フレーズ… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. 現在サポートされている最大ファイルサイズは %@. @@ -1545,11 +2056,25 @@ This is your own one-time link! カスタム時間 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 ID データベースID @@ -1648,6 +2173,11 @@ This is your own one-time link! データベースはアプリ再起動時に移行されます No comment provided by engineer. + + Debug delivery + 配信のデバッグ + No comment provided by engineer. + Decentralized 分散型 @@ -1661,17 +2191,17 @@ This is your own one-time link! Delete 削除 - chat item action + alert action +swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - 連絡先を削除 - No comment provided by engineer. - Delete address アドレスを削除 @@ -1696,14 +2226,12 @@ This is your own one-time link! 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. @@ -1716,6 +2244,10 @@ This is your own one-time link! チャットのプロフィールを削除しますか? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection 接続を削除する @@ -1726,9 +2258,8 @@ This is your own one-time link! 連絡先を削除 No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1790,6 +2321,10 @@ This cannot be undone! リンクを削除しますか? No comment provided by engineer. + + Delete list? + alert title + Delete member message? メンバーのメッセージを削除しますか? @@ -1803,7 +2338,7 @@ This cannot be undone! Delete messages メッセージを削除 - No comment provided by engineer. + alert button Delete messages after @@ -1820,9 +2355,8 @@ This cannot be undone! 古いデータベースを削除しますか? No comment provided by engineer. - - Delete pending connection - 確認待ちの接続を削除 + + Delete or moderate up to 200 messages. No comment provided by engineer. @@ -1840,11 +2374,27 @@ This cannot be undone! 待ち行列を削除 server test step + + Delete report + No comment provided by engineer. + + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? ユーザープロフィールを削除しますか? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at 削除完了 @@ -1855,6 +2405,14 @@ This cannot be undone! 削除完了: %@ copied message info + + Deletion errors + No comment provided by engineer. + + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery 配信 @@ -1885,6 +2443,27 @@ This cannot be undone! Desktop devices + デスクトップ機器 + No comment provided by engineer. + + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + 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. @@ -1892,6 +2471,11 @@ This cannot be undone! 開発 No comment provided by engineer. + + Developer options + 開発者向けの設定 + No comment provided by engineer. + Developer tools 開発ツール @@ -1922,8 +2506,12 @@ This cannot be undone! ダイレクトメッセージ chat feature - - 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. @@ -1937,11 +2525,23 @@ This cannot be undone! SimpleXロックを無効にする authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all すべて無効 No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message 消えるメッセージ @@ -1957,8 +2557,8 @@ This cannot be undone! このチャットでは消えるメッセージが使用禁止です。 No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. このグループでは消えるメッセージが使用禁止です。 No comment provided by engineer. @@ -1990,11 +2590,19 @@ This cannot be undone! Discover via local network 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 SimpleX for emergency calls. 緊急通報にSimpleXを使用しないでください。 No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later 後で行う @@ -2004,9 +2612,17 @@ This cannot be undone! Do not send history to new members. No comment provided by engineer. + + Do not use credentials with proxy. + No comment provided by engineer. + + + Documents: + No comment provided by engineer. + Don't create address - アドレスを作成しないでください + アドレスを作成しない No comment provided by engineer. @@ -2014,11 +2630,19 @@ This cannot be undone! 有効にしない 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 ダウングレードしてチャットを開く @@ -2026,7 +2650,12 @@ This cannot be undone! Download - chat item action + alert button +chat item action + + + Download errors + No comment provided by engineer. Download failed @@ -2037,6 +2666,18 @@ This cannot be undone! ファイルをダウンロード server test step + + Download files + alert action + + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive No comment provided by engineer. @@ -2055,6 +2696,10 @@ This cannot be undone! 間隔 No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit 編集する @@ -2075,6 +2720,10 @@ This cannot be undone! 有効にする(設定の優先を維持) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock SimpleXロックを有効にする @@ -2088,7 +2737,7 @@ This cannot be undone! Enable automatic message deletion? 自動メッセージ削除を有効にしますか? - No comment provided by engineer. + alert title Enable camera access @@ -2133,6 +2782,10 @@ This cannot be undone! 自己破壊パスコードを有効にする set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2204,6 +2857,10 @@ This cannot be undone! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode パスコードを入力 @@ -2265,26 +2922,33 @@ This cannot be undone! アドレス変更中止エラー No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request 連絡先リクエストの承諾にエラー発生 No comment provided by engineer. - - Error accessing database file - データベースファイルへのアクセスエラー - No comment provided by engineer. - Error adding member(s) メンバー追加にエラー発生 No comment provided by engineer. + + Error adding server + alert title + Error changing address アドレス変更にエラー発生 No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role 役割変更にエラー発生 @@ -2295,6 +2959,18 @@ This cannot be undone! 設定変更にエラー発生 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 アドレス作成にエラー発生 @@ -2310,6 +2986,10 @@ This cannot be undone! グループリンク生成にエラー発生 No comment provided by engineer. + + Error creating list + alert title + Error creating member contact メンバー連絡先の作成中にエラーが発生 @@ -2324,6 +3004,10 @@ This cannot be undone! プロフィール作成にエラー発生! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file ファイルの復号エラー @@ -2344,11 +3028,6 @@ This cannot be undone! 接続の削除エラー No comment provided by engineer. - - Error deleting contact - 連絡先の削除にエラー発生 - No comment provided by engineer. - Error deleting database データベースの削除にエラー発生 @@ -2392,6 +3071,10 @@ This cannot be undone! チャットデータベースのエキスポートにエラー発生 No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database チャットデータベースのインポートにエラー発生 @@ -2402,9 +3085,12 @@ This cannot be undone! グループ参加にエラー発生 No comment provided by engineer. - - Error loading %@ servers - %@ サーバーのロード中にエラーが発生 + + Error loading servers + alert title + + + Error migrating settings No comment provided by engineer. @@ -2414,16 +3100,31 @@ This cannot be undone! Error receiving file ファイル受信にエラー発生 + alert title + + + Error reconnecting server No comment provided by engineer. + + Error reconnecting servers + No comment provided by engineer. + + + Error registering for notifications + alert title + Error removing member メンバー除名にエラー発生 No comment provided by engineer. - - Error saving %@ servers - %@ サーバの保存エラー + + Error reordering lists + alert title + + + Error resetting statistics No comment provided by engineer. @@ -2431,6 +3132,10 @@ This cannot be undone! ICEサーバ保存にエラー発生 No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile グループのプロフィール保存にエラー発生 @@ -2446,6 +3151,10 @@ This cannot be undone! キーチェーンにパスフレーズを保存にエラー発生 No comment provided by engineer. + + Error saving servers + alert title + Error saving settings when migrating @@ -2488,16 +3197,24 @@ This cannot be undone! チャット停止にエラー発生 No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! プロフィール切り替えにエラー発生! - No comment provided by engineer. + alertTitle Error synchronizing connection 接続の同期エラー No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link グループのリンクのアップデートにエラー発生 @@ -2508,6 +3225,10 @@ This cannot be undone! メッセージの更新にエラー発生 No comment provided by engineer. + + Error updating server + alert title + Error updating settings 設定の更新にエラー発生 @@ -2534,7 +3255,9 @@ This cannot be undone! Error: %@ エラー : %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2546,6 +3269,14 @@ This cannot be undone! エラー: データベースが存在しません No comment provided by engineer. + + Errors + No comment provided by engineer. + + + Errors in servers configuration. + servers error + Even when disabled in the conversation. 会話中に無効になっている場合でも。 @@ -2560,6 +3291,10 @@ This cannot be undone! Expand chat item action + + Expired + token status text + Export database データベースをエキスポート @@ -2570,6 +3305,10 @@ This cannot be undone! エクスポートエラー: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. データベースのアーカイブをエクスポートします。 @@ -2594,15 +3333,57 @@ This cannot be undone! 送信者がオンラインになるまでの待ち時間がなく、速い! 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. + No comment provided by engineer. + Favorite お気に入り + swipe action + + + Favorites No comment provided by engineer. + + File error + file error alert title + + + File errors: +%@ + alert message + + + File is blocked by server operator: +%@. + file error text + + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. ファイルはサーバーから削除されます。 @@ -2623,6 +3404,10 @@ This cannot be undone! ファイル: %@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media ファイルとメディア @@ -2633,8 +3418,8 @@ This cannot be undone! ファイルとメディア chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. このグループでは、ファイルとメディアは禁止されています。 No comment provided by engineer. @@ -2700,19 +3485,59 @@ This cannot be undone! グループメンバーによる修正はサポートされていません No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + servers error + 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. + No comment provided by engineer. + + + For me + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward chat item action + + Forward %d message(s)? + alert title + Forward and save messages No comment provided by engineer. + + Forward messages + alert action + + + Forward messages without files? + alert message + + + Forward up to 20 messages at once. + No comment provided by engineer. + Forwarded No comment provided by engineer. @@ -2721,6 +3546,32 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding %lld messages + 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop No comment provided by engineer. @@ -2740,11 +3591,6 @@ This cannot be undone! フルネーム (任意): No comment provided by engineer. - - Full name: - フルネーム: - No comment provided by engineer. - Fully decentralized – visible only to members. No comment provided by engineer. @@ -2764,6 +3610,18 @@ This cannot be undone! GIFとステッカー No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + message preview + + + Good morning! + message preview + Group グループ @@ -2817,40 +3675,6 @@ This cannot be undone! グループのリンク No comment provided by engineer. - - Group members can add message reactions. - グループメンバーはメッセージへのリアクションを追加できます。 - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. (24 hours) - グループのメンバーがメッセージを完全削除することができます。(24時間) - No comment provided by engineer. - - - Group members can send SimpleX links. - 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. - グループのメンバーが音声メッセージを送信できます。 - No comment provided by engineer. - Group message: グループメッセージ: @@ -2891,11 +3715,19 @@ 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 プライベート @@ -2945,10 +3777,17 @@ This cannot be undone! SimpleX の仕組み No comment provided by engineer. + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy + No comment provided by engineer. + How it works - 技術の説明 - No comment provided by engineer. + alert button How to @@ -2974,6 +3813,10 @@ This cannot be undone! ICEサーバ (1行に1サーバ) No comment provided by engineer. + + IP address + No comment provided by engineer. + If you can't meet in person, show QR code in a video call, or share the link. 直接会えない場合は、ビデオ通話で QR コードを表示するか、リンクを共有してください。 @@ -3014,8 +3857,8 @@ This cannot be undone! 即座に No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam スパムや悪質送信を防止 No comment provided by engineer. @@ -3038,10 +3881,19 @@ This cannot be undone! 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. +More improvements are coming soon! + No comment provided by engineer. + Improved message delivery No comment provided by engineer. @@ -3069,6 +3921,14 @@ This cannot be undone! In-call sounds No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito シークレットモード @@ -3137,6 +3997,11 @@ This cannot be undone! インストール [ターミナル用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! @@ -3144,16 +4009,35 @@ This cannot be undone! No comment provided by engineer. - - Instantly - すぐに - No comment provided by engineer. - Interface インターフェース No comment provided by engineer. + + Interface colors + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code No comment provided by engineer. @@ -3186,7 +4070,7 @@ This cannot be undone! Invalid server address! 無効なサーバアドレス! - No comment provided by engineer. + alert title Invalid status @@ -3208,6 +4092,10 @@ This cannot be undone! メンバーを招待する No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group グループに招待する @@ -3223,8 +4111,8 @@ This cannot be undone! このチャットではメッセージの完全削除が使用禁止です。 No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. このグループではメッセージの完全削除が使用禁止です。 No comment provided by engineer. @@ -3249,6 +4137,10 @@ This cannot be undone! 3. 接続に問題があった。 No comment provided by engineer. + + It protects your IP address and connections. + 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 (%@). このリンクからすでに接続されているようです。そうでない場合は、エラー(%@)が発生しました。 @@ -3267,7 +4159,7 @@ This cannot be undone! Join 参加 - No comment provided by engineer. + swipe action Join group @@ -3303,6 +4195,10 @@ This is your link for group %@! Keep + alert action + + + Keep conversation No comment provided by engineer. @@ -3311,7 +4207,7 @@ This is your link for group %@! Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3346,6 +4242,14 @@ This is your link for group %@! Leave 脱退 + swipe action + + + Leave chat + No comment provided by engineer. + + + Leave chat? No comment provided by engineer. @@ -3385,6 +4289,18 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! ライブメッセージ! @@ -3395,11 +4311,6 @@ This is your link for group %@! ライブメッセージ No comment provided by engineer. - - Local - 自分のみ - No comment provided by engineer. - Local name ローカルネーム @@ -3420,11 +4331,6 @@ This is your link for group %@! ロックモード No comment provided by engineer. - - Make a private connection - プライベートな接続をする - No comment provided by engineer. - Make one message disappear メッセージを1つ消す @@ -3435,21 +4341,11 @@ This is your link for group %@! プロフィールを非表示にできます! 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 全員に対して削除済みマークを付ける @@ -3475,11 +4371,31 @@ This is your link for group %@! 最大 30 秒で即時受信します。 No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member メンバー No comment provided by engineer. + + Member inactive + item status text + + + Member reports + chat feature + + + 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. メンバーの役割が "%@" に変更されます。 グループメンバー全員に通知されます。 @@ -3490,11 +4406,61 @@ This is your link for group %@! メンバーの役割が "%@" に変更されます。 メンバーは新たな招待を受け取ります。 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 report messsages to moderators. + No comment provided by engineer. + + + Members can send SimpleX links. + 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. + グループのメンバーが音声メッセージを送信できます。 + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + No comment provided by engineer. + Message delivery error メッセージ送信エラー @@ -3504,11 +4470,27 @@ This is your link for group %@! Message delivery receipts! No comment provided by engineer. + + Message delivery warning + item status text + Message draft メッセージの下書き No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions メッセージへのリアクション @@ -3519,15 +4501,35 @@ This is your link for group %@! このチャットではメッセージへのリアクションは禁止されています。 No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. このグループではメッセージへのリアクションは禁止されています。 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 + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text メッセージ内容 @@ -3551,12 +4553,30 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + + + Messages were deleted after you selected them. + alert message + 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. @@ -3565,6 +4585,7 @@ This is your link for group %@! Migrate from another device + 別の端末から移行 No comment provided by engineer. @@ -3607,9 +4628,9 @@ This is your link for group %@! 移行が完了しました No comment provided by engineer. - - Migrations: %@ - 移行: %@ + + Migrations: + 移行: No comment provided by engineer. @@ -3627,6 +4648,10 @@ This is your link for group %@! モデレーターによって介入済み: %@ copied message info + + More + swipe action + More improvements are coming soon! まだまだ改善してまいります! @@ -3636,16 +4661,15 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. おそらく、この接続は削除されています。 item status description - - Most likely this contact has deleted the connection with you. - 恐らくこの連絡先があなたとの接続を削除しました。 - No comment provided by engineer. - Multiple chat profiles 複数チャットのプロフィール @@ -3654,7 +4678,11 @@ This is your link for group %@! Mute ミュート - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3664,7 +4692,7 @@ This is your link for group %@! Name 名前 - No comment provided by engineer. + swipe action Network & servers @@ -3675,10 +4703,22 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + snd error text + Network management No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings ネットワーク設定 @@ -3689,15 +4729,31 @@ This is your link for group %@! ネットワーク状況 No comment provided by engineer. + + New + token status text + New Passcode 新しいパスコード No comment provided by engineer. + + New SOCKS credentials will be used every time you start the app. + No comment provided by engineer. + + + New SOCKS credentials will be used for each server. + No comment provided by engineer. + New chat No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request 新しい繋がりのリクエスト @@ -3708,11 +4764,6 @@ This is your link for group %@! 新しい連絡先: notification - - New database archive - 新しいデータベースのアーカイブ - No comment provided by engineer. - New desktop app! 新しいデスクトップアプリ! @@ -3723,11 +4774,19 @@ This is your link for group %@! 新たな表示名 No comment provided by engineer. + + New events + notification + New in %@ %@ の新機能 No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role 新しいメンバーの役割 @@ -3743,6 +4802,10 @@ This is your link for group %@! 新しいパスフレーズ… No comment provided by engineer. + + New server + No comment provided by engineer. + No いいえ @@ -3753,6 +4816,18 @@ This is your link for group %@! アプリのパスワードはありません Authentication unavailable + + 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 連絡先が選択されてません @@ -3773,6 +4848,10 @@ This is your link for group %@! デバイストークンがありません! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats フィルタされたチャットはありません @@ -3788,24 +4867,94 @@ This is your link for group %@! 履歴はありません No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + + + No media & file servers. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + servers error + 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 送受信済みのファイルがありません No comment provided by engineer. + + 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. + servers error + + + No token! + alert title + + + No unread chats + No comment provided by engineer. + + + No user identifiers. + 世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。 + No comment provided by engineer. + Not compatible! No comment provided by engineer. + + Notes + No comment provided by engineer. + + + Nothing selected + No comment provided by engineer. + + + Nothing to forward! + alert title + Notifications 通知 @@ -3816,6 +4965,18 @@ This is your link for group %@! 通知が無効になっています! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3832,36 +4993,35 @@ This is your link for group %@! Off オフ - No comment provided by engineer. + blur media Ok OK - No comment provided by engineer. + alert button Old database 古いデータベース No comment provided by engineer. - - Old database archive - 過去のデータベースアーカイブ - No comment provided by engineer. - One-time invitation link 使い捨ての招待リンク No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - 接続にオニオンのホストが必要となります。VPN を有効にする必要があります。 + + Onion hosts will be **required** for connection. +Requires 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. +Requires compatible VPN. + オニオンのホストが利用可能時に使われます。 +VPN を有効にする必要があります。 No comment provided by engineer. @@ -3869,11 +5029,19 @@ This is your link for group %@! オニオンのホストが使われません。 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. **2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。 No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. グループ設定を変えられるのはグループのオーナーだけです。 @@ -3889,6 +5057,14 @@ This is your link for group %@! 音声メッセージを利用可能に設定できるのはグループのオーナーだけです。 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. メッセージへのリアクションを追加できるのは、あなただけです。 @@ -3942,13 +5118,17 @@ This is your link for group %@! Open 開く - No comment provided by engineer. + alert action Open Settings 設定を開く No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat チャットを開く @@ -3959,28 +5139,38 @@ This is your link for group %@! チャットのコンソールを開く authentication reason + + Open conditions + No comment provided by engineer. + Open group No comment provided by engineer. + + Open link? + alert title + Open migration to another device authentication reason - - Open user profiles - ユーザープロフィールを開く - authentication reason - - - Open-source protocol and code – anybody can run the servers. - プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。 - No comment provided by engineer. - Opening app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + + + Or import archive file + No comment provided by engineer. + Or paste archive link No comment provided by engineer. @@ -3997,10 +5187,23 @@ This is your link for group %@! 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 file errors: +%@ + alert message + PING count PING回数 @@ -4036,6 +5239,10 @@ This is your link for group %@! パスコードを設定しました! No comment provided by engineer. + + Password + No comment provided by engineer. + Password to show パスワードを表示する @@ -4062,13 +5269,12 @@ This is your link for group %@! Paste the link you received No comment provided by engineer. - - People can connect to you only via the links you share. - あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 + + Pending No comment provided by engineer. - - Periodically + + Periodic 定期的に No comment provided by engineer. @@ -4081,11 +5287,24 @@ This is your link for group %@! Picture-in-picture calls 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. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. リンクが正しいかどうかご確認ください。または、連絡相手にもう一度リンクをお求めください。 @@ -4150,11 +5369,27 @@ Error: %@ パスフレーズを失くさないように保管してください。失くすと変更できなくなります。 No comment provided by engineer. + + 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. + token info + Polish interface ポーランド語UI No comment provided by engineer. + + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect サーバアドレスの証明証IDが正しくないかもしれません @@ -4165,40 +5400,76 @@ Error: %@ 添付を含めて、下書きを保存する。 No comment provided by engineer. - - Preset server - プレセットサーバ - No comment provided by engineer. - Preset server address プレセットサーバのアドレス No comment provided by engineer. + + 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 🚀 + No comment provided by engineer. + Private notes + プライベートノート name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections プロフィールとサーバ接続 @@ -4213,23 +5484,19 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - No comment provided by engineer. - Profile password プロフィールのパスワード No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. 連絡先にプロフィール更新のお知らせが届きます。 - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4251,6 +5518,10 @@ Error: %@ メッセージへのリアクションは禁止されています。 No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. No comment provided by engineer. @@ -4275,11 +5546,20 @@ Error: %@ 音声メッセージを使用禁止にする。 No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen アプリ画面を守る No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! チャットのプロフィールをパスワードで保護します! @@ -4295,6 +5575,18 @@ Error: %@ 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 プッシュ通知 @@ -4313,6 +5605,10 @@ Error: %@ アプリを評価 No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… 反応する… @@ -4321,41 +5617,40 @@ Error: %@ Read 読む - No comment provided by engineer. + swipe action Read more 続きを読む No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - 詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。 - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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)をご覧ください。 No comment provided by engineer. - - Read more in our GitHub repository. - GitHubリポジトリで詳細をご確認ください。 - 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)をご覧ください。 + 詳しくは[GitHubリポジトリ](https://github.com/simplex-chat/simplex-chat#readme)をご覧ください。 No comment provided by engineer. Receipts are disabled No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at 受信 @@ -4376,15 +5671,23 @@ Error: %@ 受信したメッセージ message info title + + 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. 開発中の機能です!相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。 No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. ファイルの受信を停止します。 @@ -4408,11 +5711,31 @@ Error: %@ 受信者には、入力時に更新内容が表示されます。 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? サーバーに再接続しますか? @@ -4433,10 +5756,23 @@ Error: %@ 電池使用量低減 No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject 拒否 - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4463,6 +5799,14 @@ Error: %@ 削除 No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + + + Remove image + No comment provided by engineer. + Remove member メンバーを除名する @@ -4518,6 +5862,46 @@ Error: %@ 返信 chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required 必須 @@ -4528,16 +5912,36 @@ Error: %@ 戻す 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 新しいチャットプロファイルを作成するためにアプリを再起動する @@ -4577,9 +5981,8 @@ Error: %@ 開示する chat item action - - Revert - 元に戻す + + Review conditions No comment provided by engineer. @@ -4607,9 +6010,16 @@ Error: %@ チャット起動 No comment provided by engineer. - - SMP servers - SMPサーバ + + SMP server + No comment provided by engineer. + + + SOCKS proxy + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4619,43 +6029,42 @@ Error: %@ Save 保存 - chat item action + alert button +chat item action Save (and notify contacts) 保存(連絡先に通知) - No comment provided by engineer. + alert button Save and notify contact 保存して、連絡先にに知らせる - No comment provided by engineer. + alert button 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 パスフレーズをを保存して、チャットを開始 @@ -4669,7 +6078,7 @@ Error: %@ Save preferences? この設定でよろしいですか? - No comment provided by engineer. + alert title Save profile password @@ -4684,18 +6093,17 @@ Error: %@ Save servers? サーバを保存しますか? - No comment provided by engineer. - - - Save settings? - 設定を保存しますか? - No comment provided by engineer. + alert title Save welcome message? ウェルカムメッセージを保存しますか? No comment provided by engineer. + + Save your profile? + alert title + Saved No comment provided by engineer. @@ -4713,6 +6121,18 @@ Error: %@ Saved message message info title + + Saving %lld messages + No comment provided by engineer. + + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code QRコードを読み込む @@ -4750,11 +6170,19 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue 待ち行列セキュリティ確認 server test step + + Secured + No comment provided by engineer. + Security assessment セキュリティ評価 @@ -4768,6 +6196,18 @@ Error: %@ Select 選択 + chat item action + + + Select chat profile + No comment provided by engineer. + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4804,11 +6244,6 @@ Error: %@ Send delivery receipts to No comment provided by engineer. - - Send direct message - ダイレクトメッセージを送信 - No comment provided by engineer. - Send direct message to connect ダイレクトメッセージを送信して接続する @@ -4819,6 +6254,10 @@ Error: %@ 消えるメッセージを送信 No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews リンクのプレビューを送信 @@ -4829,14 +6268,25 @@ Error: %@ ライブメッセージを送信 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. + 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. @@ -4860,7 +6310,7 @@ Error: %@ Sender cancelled file transfer. 送信者がファイル転送をキャンセルしました。 - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -4911,6 +6361,10 @@ Error: %@ 送信日時: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event 送信済みファイルイベント @@ -4921,11 +6375,59 @@ Error: %@ 送信 message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + alert message + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password キューを作成するにはサーバーの認証が必要です。パスワードを確認してください @@ -4941,11 +6443,31 @@ Error: %@ サーバテスト失敗! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + 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. @@ -4955,11 +6477,19 @@ Error: %@ 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 グループの設定を行う @@ -4970,6 +6500,10 @@ Error: %@ システム認証の代わりに設定します。 No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode パスコードを設定する @@ -4999,6 +6533,10 @@ Error: %@ 設定 No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images No comment provided by engineer. @@ -5006,21 +6544,38 @@ Error: %@ Share 共有する - chat item action + alert action +chat item action Share 1-time link 使い捨てのリンクを共有 No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address アドレスを共有する No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? アドレスを連絡先と共有しますか? + alert title + + + Share from other apps. No comment provided by engineer. @@ -5028,15 +6583,27 @@ Error: %@ リンクを送る No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts 連絡先と共有する No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -5056,21 +6623,41 @@ Error: %@ 最新のメッセージを表示 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 → on messages sent via private routing. + No comment provided by engineer. + Show: 表示する: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleXアドレス No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. SimpleX Chat のセキュリティは Trail of Bits によって監査されました。 @@ -5101,6 +6688,18 @@ Error: %@ SimpleXアドレス No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX連絡先アドレス @@ -5121,8 +6720,8 @@ Error: %@ SimpleXリンク chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. No comment provided by engineer. @@ -5134,11 +6733,19 @@ Error: %@ SimpleX使い捨て招待リンク simplex link type + + SimpleX protocols reviewed by Trail of Bits. + No comment provided by engineer. + Simplified incognito mode シークレットモードの簡素化 No comment provided by engineer. + + Size + No comment provided by engineer. + Skip スキップ @@ -5154,16 +6761,42 @@ Error: %@ 小グループ(最大20名) No comment provided by engineer. + + Soft + blur media + + + 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + alert message + Somebody 誰か notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. No comment provided by engineer. @@ -5182,6 +6815,14 @@ Error: %@ 移行の開始 No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop 停止 @@ -5196,11 +6837,6 @@ Error: %@ Stop chat 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. データベースのエクスポート、読み込み、削除するにはチャットを閉じてからです。チャットを閉じると送受信ができなくなります。 @@ -5229,27 +6865,55 @@ Error: %@ Stop sharing 共有を停止 - No comment provided by engineer. + alert action Stop sharing address? アドレスの共有を停止しますか? - No comment provided by engineer. + alert title Stopping chat No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + blur media + 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 システム @@ -5260,11 +6924,19 @@ Error: %@ システム認証 No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout TCP接続タイムアウト No comment provided by engineer. + + TCP port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5280,11 +6952,19 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture 写真を撮影 No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button ボタンをタップ @@ -5317,16 +6997,19 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - タップして新しいチャットを始める - No comment provided by engineer. + + Temporary file error + file error alert title Test failed at step %@. テストはステップ %@ で失敗しました。 server test failure + + Test notifications + No comment provided by engineer. + Test server テストサーバ @@ -5340,7 +7023,7 @@ Error: %@ Tests failed! テストは失敗しました! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5357,11 +7040,6 @@ Error: %@ ユーザーに感謝します – Weblate 経由で貢献してください! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - 世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。 - No comment provided by engineer. - 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. @@ -5374,6 +7052,14 @@ It can happen because of some bug or when the connection is compromised.アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。 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). + No comment provided by engineer. + The attempt to change database passphrase was not completed. データベースのパスフレーズ変更が完了してません。 @@ -5383,6 +7069,10 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! 承認済の接続がキャンセルされます! @@ -5403,6 +7093,11 @@ It can happen because of some bug or when the connection is compromised.暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります! No comment provided by engineer. + + The future of messaging + 次世代のプライバシー・メッセンジャー + No comment provided by engineer. + The hash of the previous message is different. 以前のメッセージとハッシュ値が異なります。 @@ -5418,9 +7113,12 @@ It can happen because of some bug or when the connection is compromised.メッセージは、すべてのメンバーに対してモデレートされたものとして表示されます。 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. @@ -5428,9 +7126,12 @@ It can happen because of some bug or when the connection is compromised.古いデータベースは移行時に削除されなかったので、削除することができます。 No comment provided by engineer. - - The profile is only shared with your contacts. - プロフィールは連絡先にしか共有されません。 + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! No comment provided by engineer. @@ -5448,13 +7149,24 @@ It can happen because of some bug or when the connection is compromised.現在のチャットプロフィールの新しい接続のサーバ **%@**。 No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - テーマ + + 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. @@ -5477,6 +7189,10 @@ It can happen because of some bug or when the connection is compromised.選択中の以前の送受信したメッセージが削除されます (※元に戻せません※)。数分かかります。 No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。 @@ -5515,11 +7231,27 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! 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. + 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: 質問や最新情報を受け取るには: @@ -5539,9 +7271,8 @@ It can happen because of some bug or when the connection is compromised.新規に接続する場合 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 protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -5549,6 +7280,10 @@ It can happen because of some bug or when the connection is compromised.時間帯を漏らさないために、画像と音声ファイルはUTCを使います。 No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5556,6 +7291,23 @@ You will be prompted to complete authentication before this feature is enabled.< オンにするには、認証ステップが行われます。 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 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. 音声メッセージを録音する場合は、マイクの使用を許可してください。 @@ -5566,25 +7318,53 @@ You will be prompted to complete authentication before this feature is enabled.< 非表示のプロフィールを表示するには、**チャット プロフィール** ページの検索フィールドに完全なパスワードを入力します。 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. エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。 No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. + + Token status: %@. + token status + + + 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: %@). この連絡先からのメッセージの受信に使用されるサーバーに接続しようとしています (エラー: %@)。 @@ -5634,10 +7414,9 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. - - Unexpected error: %@ - 予期しないエラー: %@ - item status description + + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5647,7 +7426,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. お気に入りを取り消す。 - No comment provided by engineer. + swipe action Unhide @@ -5684,6 +7463,10 @@ You will be prompted to complete authentication before this feature is enabled.< 不明なエラー No comment provided by engineer. + + Unknown servers! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. iOS 通話インターフェイスを使用しない場合は、中断を避けるために「おやすみモード」を有効にしてください。 @@ -5717,11 +7500,15 @@ To connect, please ask your contact to create another connection link and check Unmute ミュート解除 - No comment provided by engineer. + notification label action Unread 未読 + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5733,11 +7520,6 @@ To connect, please ask your contact to create another connection link and check 更新 No comment provided by engineer. - - Update .onion hosts setting? - .onionのホスト設定を更新しますか? - No comment provided by engineer. - Update database passphrase データベースのパスフレーズを更新 @@ -5748,9 +7530,12 @@ To connect, please ask your contact to create another connection link and check ネットワーク設定を更新しますか? No comment provided by engineer. - - Update transport isolation mode? - トランスポート隔離モードを更新しますか? + + Update settings? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5758,16 +7543,15 @@ To connect, please ask your contact to create another connection link and check 設定を更新すると、全サーバにクライントの再接続が行われます。 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 アップグレードしてチャットを開く No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5777,20 +7561,44 @@ To connect, please ask your contact to create another connection link and check ファイルをアップロードする server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts .onionホストを使う No comment provided by engineer. + + Use SOCKS proxy + No comment provided by engineer. + Use SimpleX Chat servers? SimpleX チャット サーバーを使用しますか? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat チャット @@ -5801,6 +7609,14 @@ To connect, please ask your contact to create another connection link and check 現在のプロファイルを使用する 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 新しい接続に使う @@ -5824,23 +7640,45 @@ To connect, please ask your contact to create another connection link and check Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + 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 short links (BETA) + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. - - User profile - ユーザープロフィール + + Use the app with one hand. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - .onionホストを使用するには、互換性のあるVPNプロバイダーが必要です。 + + Use web port + No comment provided by engineer. + + + User selection + No comment provided by engineer. + + + Username No comment provided by engineer. @@ -5907,11 +7745,19 @@ To connect, please ask your contact to create another connection link and check 1GBまでのビデオとファイル No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code セキュリティコードを確認 No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history chat feature @@ -5926,8 +7772,8 @@ To connect, please ask your contact to create another connection link and check このチャットでは音声メッセージが使用禁止です。 No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. このグループでは音声メッセージが使用禁止です。 No comment provided by engineer. @@ -5964,6 +7810,14 @@ To connect, please ask your contact to create another connection link and check ビデオ待機中 No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + 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. @@ -6006,9 +7860,8 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - 接続が要求されたら、それを受け入れるか拒否するかを選択できます。 + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -6041,24 +7894,34 @@ To connect, please ask your contact to create another connection link and check With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + alert message + Wrong database passphrase データベースのパスフレーズが違います No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! パスフレーズが違います! No comment provided by engineer. - - XFTP servers - XFTPサーバ - No comment provided by engineer. - - - You - あなた + + XFTP server No comment provided by engineer. @@ -6085,6 +7948,10 @@ To connect, please ask your contact to create another connection link and check すでに %@ に接続されています。 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. @@ -6124,11 +7991,23 @@ Repeat join request? グループ招待が届きました 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 can accept calls from lock screen, without device and app authentication. デバイスやアプリの認証を行わずに、ロック画面から通話を受けることができます。 No comment provided by engineer. + + 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 後からでも作成できます @@ -6155,13 +8034,22 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. + 設定でSimpleXの連絡先に表示させることができます。 No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ %@ にメッセージを送信できるようになりました notification body + + 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. 設定からロック画面の通知プレビューを設定できます。 @@ -6177,16 +8065,15 @@ Repeat join request? このアドレスを連絡先と共有して、**%@** に接続できるようにすることができます。 No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - アドレスをリンクやQRコードとして共有することで、誰でも接続することができます。 - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app アプリの設定/データベースから、またはアプリを再起動することでチャットを開始できます No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. 設定からSimpleXのロックをオンにすることができます。 @@ -6199,23 +8086,23 @@ Repeat join request? You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! メッセージを送信できませんでした! 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 decide who can connect. + あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 + No comment provided by engineer. + You have already requested connection via this address! No comment provided by engineer. @@ -6225,11 +8112,6 @@ Repeat join request? Repeat connection request? 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. アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。 @@ -6250,11 +8132,23 @@ Repeat connection request? グループに参加しました。招待をくれたメンバーに接続してます。 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. あなたの最新データベースを1つの端末にしか使わなければ、一部の連絡先からメッセージが届きかねます。 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. 音声メッセージを送るには、連絡相手からの音声メッセージを許可しなければなりません。 @@ -6270,6 +8164,10 @@ Repeat connection request? グループの招待を送りました No comment provided by engineer. + + You should receive notifications. + token info + You will be connected to group when the group host's device is online, please wait or check later! グループのホスト端末がオンラインになったら、接続されます。後でチェックするか、しばらくお待ちください! @@ -6303,6 +8201,10 @@ Repeat connection request? ミュートされたプロフィールがアクティブな場合でも、そのプロフィールからの通話や通知は引き続き受信します。 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. このグループからのメッセージが届かなくなります。チャットの履歴が残ります。 @@ -6323,31 +8225,16 @@ Repeat connection request? シークレットモードのプロフィールでこのグループに参加しています。メインのプロフィールを守るために、招待することができません No comment provided by engineer. - - Your %@ servers - あなたの %@ サーバー - No comment provided by engineer. - Your ICE servers あなたのICEサーバ No comment provided by engineer. - - Your SMP servers - あなたのSMPサーバ - No comment provided by engineer. - Your SimpleX address あなたのSimpleXアドレス No comment provided by engineer. - - Your XFTP servers - あなたのXFTPサーバ - No comment provided by engineer. - Your calls あなたの通話 @@ -6363,16 +8250,17 @@ Repeat connection request? チャット データベースは暗号化されていません - 暗号化するにはパスフレーズを設定してください。 No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles あなたのチャットプロフィール No comment provided by engineer. - - 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). - 接続を完了するには、連絡相手がオンラインになる必要があります。 -この接続をキャンセルして、連絡先を削除をすることもできます (後でやり直すこともできます)。 + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. No comment provided by engineer. @@ -6390,6 +8278,10 @@ You can cancel this connection and remove the contact (and try later with a new 連絡先は接続されたままになります。 No comment provided by engineer. + + Your credentials may be sent unencrypted. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. 現在のチャット データベースは削除され、インポートされたデータベースに置き換えられます。 @@ -6419,33 +8311,34 @@ You can cancel this connection and remove the contact (and try later with a new あなたのプロファイル **%@** が共有されます。 No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - プロフィールはデバイスに保存され、連絡先とのみ共有されます。 -SimpleX サーバーはあなたのプロファイルを参照できません。 + + Your profile is stored on your device and only shared with your contacts. + プロフィールは連絡先にしか共有されません。 No comment provided by engineer. - - Your profile, contacts and delivered messages are stored on your device. - あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。 + + 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 was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your random profile あなたのランダム・プロフィール No comment provided by engineer. - - Your server - あなたのサーバ - No comment provided by engineer. - Your server address あなたのサーバアドレス No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings あなたの設定 @@ -6486,6 +8379,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 受けた通話 call status + + accepted invitation + chat list item title + admin 管理者 @@ -6518,6 +8415,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 and %lld other events No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) 音声通話 (エンドツーエンド暗号化なし) @@ -6547,13 +8452,18 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold 太文字 No comment provided by engineer. + + call + No comment provided by engineer. + call error 通話エラー @@ -6656,7 +8566,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 connecting… 接続待ち… - chat list item title + No comment provided by engineer. connection established @@ -6702,10 +8612,15 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 time unit + + decryption errors + No comment provided by engineer. + default (%@) デフォルト (%@) - pref value + delete after time +pref value default (no) @@ -6751,6 +8666,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 重複メッセージ integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted エンドツーエンド暗号化済み @@ -6826,9 +8745,8 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 エラー No comment provided by engineer. - - event happened - イベント発生 + + expired No comment provided by engineer. @@ -6860,6 +8778,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 iOS キーチェーンは、アプリを再起動するかパスフレーズを変更した後にパスフレーズを安全に保存するために使用され、プッシュ通知を受信できるようになります。 No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link 連絡先リンク経由でシークレットモード @@ -6900,6 +8822,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 グループ %@ への招待 group name + + invite + No comment provided by engineer. + invited 招待済み @@ -6954,6 +8880,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 接続中 rcv group event chat item + + message + No comment provided by engineer. + message received メッセージを受信 @@ -6979,6 +8909,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 %@ によってモデレートされた marked deleted chat item preview text + + moderator + member role + months @@ -6987,7 +8921,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 never 一度も - No comment provided by engineer. + delete after time new message @@ -7018,8 +8952,8 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 off オフ enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7036,6 +8970,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 オン group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner オーナー @@ -7050,6 +8992,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 P2P No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -7064,6 +9014,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 確認を受け取りました… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call 拒否した通話 @@ -7092,6 +9046,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 あなたを除名しました rcv group event chat item + + requested to connect + chat list item title + saved No comment provided by engineer. @@ -7100,6 +9058,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec @@ -7124,6 +9086,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 send direct message No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address profile update event chat item @@ -7160,10 +9128,18 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 不明 connection info + + unknown servers + No comment provided by engineer. + unknown status No comment provided by engineer. + + unprotected + No comment provided by engineer. + updated group profile グループプロフィールを更新しました @@ -7202,6 +9178,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 リレー経由 No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) ビデオ通話 (非エンドツーエンド暗号化) @@ -7227,6 +9207,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 time unit + + when IP hidden + No comment provided by engineer. + yes はい @@ -7308,7 +9292,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
- +
@@ -7344,7 +9328,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
- +
@@ -7364,4 +9348,205 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
+ +
+ +
+ + + %d new events + notification body + + + From %d chat(s) + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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 + 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. + No comment provided by engineer. + + +
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 @@ - + + + No comment provided by engineer. @@ -22,220 +24,264 @@ No comment provided by engineer. - + ( + ( No comment provided by engineer. - + (can be copied) + (복사 가능) No comment provided by engineer. - + !1 colored! + !1 색상 적용됨! No comment provided by engineer. - + #secret# + #비밀# No comment provided by engineer. - + %@ + %@ No comment provided by engineer. - + %@ %@ + %@ %@ No comment provided by engineer. - + %@ / %@ + %@ / %@ No comment provided by engineer. - + %@ is connected! + %@이(가) 연결되었습니다! notification title - + %@ is not verified + %@은(는) 인증되지 않았습니다 No comment provided by engineer. - + %@ is verified + %@ 은(는) 인증되었습니다 No comment provided by engineer. - + %@ wants to connect! + %@ 연결을 원함! notification title - + %d days + %d 일 message ttl - + %d hours + %d 시간 message ttl - + %d min + %d 분 message ttl - + %d months + %d 개월 message ttl - + %d sec + %d 초 message ttl - + %d skipped message(s) + 건너뛰기 메시지 %d개 integrity error chat item - + %lld + %lld No comment provided by engineer. - + %lld %@ + %lld %@ No comment provided by engineer. - + %lld contact(s) selected + %lld명의 연락처 선택됨 No comment provided by engineer. - + %lld file(s) with total size of %@ + 총 크기가 %@인 파일 %lld 개 No comment provided by engineer. - + %lld members + %lld명의 멤버 No comment provided by engineer. - + %lld second(s) + %lld 초 No comment provided by engineer. - + %lldd + %lldd No comment provided by engineer. - + %lldh + %lldh No comment provided by engineer. - + %lldk + %lldk No comment provided by engineer. - + %lldm + %lldm No comment provided by engineer. - + %lldmth + %lldmth No comment provided by engineer. %llds + No comment provided by engineer. %lldw No comment provided by engineer. - + ( + ( No comment provided by engineer. - + ) - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. + ) No comment provided by engineer. **Create link / QR code** for your contact to use. 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. + + **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분마다 새로운 메시지를 확인합니다. 푸시 서버에는 장치 토큰만 공유됩니다. 연락처 수나 메세지 메타데이터가 표시되지 않습니다. 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). + + **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 채팅 푸시 서버를 사용하지 마세요. 앱은 사용 빈도에 따라 시스템이 허용하는 백그라운드에서 메세지를 확인합니다. No comment provided by engineer. **Paste received link** or open it in the browser and tap **Open in mobile app**. No comment provided by engineer. - + **Please note**: you will NOT be able to recover or change passphrase if you lose it. + **참고**: 비밀번호를 분실하면 복구하거나 변경할 수 없습니다. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **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 채팅 푸시 서버로 전송되지만 메세지 내용, 크기 또는 발신자가 표시되지 않습니다. No comment provided by engineer. **Scan QR code**: to connect to your contact in person or via video call. No comment provided by engineer. - + **Warning**: Instant push notifications require passphrase saved in Keychain. + **경고**: 즉각적인 푸시 알림은 암호문을 키체인에 저장해야 합니다. No comment provided by engineer. - + **e2e encrypted** audio call + **e2e** 오디오 통화 No comment provided by engineer. - + **e2e encrypted** video call + **e2e 암호화** 영상 통화 No comment provided by engineer. \*bold* + No comment provided by engineer. - + , + , No comment provided by engineer. - + . + . No comment provided by engineer. - + 1 day + 1일 message ttl - + 1 hour + 1시간 message ttl - + 1 month + 1개월 message ttl - + 1 week + 1주 message ttl 2 weeks message ttl - + 6 + 6 No comment provided by engineer. - + : + : No comment provided by engineer. - + A new contact + 새로운 연결 notification title @@ -246,29 +292,34 @@ A random profile will be sent to your contact No comment provided by engineer. - + A separate TCP connection will be used **for each chat profile you have in the app**. + 앱에 있는 각 채팅 프로필**마다 별도의 TCP 연결이 사용됩니다. No comment provided by engineer. - + A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail. + 각 연락처 및 그룹 구성원**마다 별도의 TCP 연결이 사용됩니다. +**참고**: 연결이 많으면 배터리와 트래픽 소비가 상당히 증가하고 일부 연결이 실패할 수 있습니다. No comment provided by engineer. About SimpleX No comment provided by engineer. - + About SimpleX Chat + SimpleX Chat에 대하여 No comment provided by engineer. Accent color No comment provided by engineer. - + Accept + 승인 accept contact request via notification accept incoming call via notification @@ -276,12 +327,14 @@ Accept contact No comment provided by engineer. - + Accept contact request from %@? + %@의 연락 요청을 수락하시겠습니까? notification body - + Accept incognito + 인정하지 않음 No comment provided by engineer. @@ -292,337 +345,411 @@ Add preset servers No comment provided by engineer. - + Add profile + 프로필 추가하기 No comment provided by engineer. - + Add servers by scanning QR codes. + QR 코드를 스캔하여 서버를 추가합니다. No comment provided by engineer. - - Add server… + + Add server + 서버 추가하기 No comment provided by engineer. - + Add to another device + 다른 장치에 추가하기 No comment provided by engineer. - + Add welcome message + 환영 메세지 추가하기 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. - + All chats and messages will be deleted - this cannot be undone! + 모든 채팅과 메세지가 삭제됩니다. - 수정 불가능! No comment provided by engineer. - + All group members will remain connected. + 모든 그룹 구성원은 연결 상태를 유지합니다. No comment provided by engineer. - + All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. + 모든 메세지가 삭제됩니다 - 수정할 수 없습니다! 메세지는 오직 당신만을 위해 삭제될 것입니다. No comment provided by engineer. All your contacts will remain connected No comment provided by engineer. - + Allow + 승인 No comment provided by engineer. - + Allow disappearing messages only if your contact allows it to you. + 연락처가 메세지를 허용하는 경우에만 메세지 삭제를 허용합니다. No comment provided by engineer. Allow irreversible message deletion only if your contact allows it to you. No comment provided by engineer. - + Allow sending direct messages to members. + 회원에게 직접 메시지를 보낼 수 있습니다. No comment provided by engineer. - + Allow sending disappearing messages. + 사라지는 메시지를 보내는 것을 허용합니다. No comment provided by engineer. Allow to irreversibly delete sent messages. No comment provided by engineer. - + Allow to send voice messages. + 음성 메세지를 보낼 수 있습니다. No comment provided by engineer. - + Allow voice messages only if your contact allows them. + 연락처가 음성 메세지를 허용하는 경우에만 음성 메세지를 허용합니다. No comment provided by engineer. - + Allow voice messages? + 음성 메세지를 허용 하겠습니까? No comment provided by engineer. Allow your contacts to irreversibly delete sent messages. No comment provided by engineer. - + Allow your contacts to send disappearing messages. + 연락처가 사라지는 메시지를 보낼 수 있도록 허용합니다. No comment provided by engineer. - + Allow your contacts to send voice messages. + 연락처가 음성 메시지를 보낼 수 있도록 허용합니다. No comment provided by engineer. - + Already connected? + 이미 연결되었나요? No comment provided by engineer. - + Always use relay + 항상 릴레이 사용 No comment provided by engineer. - + Answer call + 응답 전화 No comment provided by engineer. - + App build: %@ + 앱 빌드: %@ No comment provided by engineer. - + App icon + 앱 아이콘 No comment provided by engineer. - + App version + 앱 버전 No comment provided by engineer. - + App version: v%@ + 앱 버전: v%@ No comment provided by engineer. - + Appearance + 출석 No comment provided by engineer. - + Attach + 첨부 No comment provided by engineer. - + Audio & video calls + 음성 & 영상 통화 No comment provided by engineer. - + Audio and video calls + 음성 및 영상 통화 No comment provided by engineer. - + Authentication failed + 인증 실패 No comment provided by engineer. - + Authentication is required before the call is connected, but you may miss calls. + 통화가 연결되기 전에 인증이 필요하지만, 통화를 놓칠 수 있습니다. No comment provided by engineer. - + Authentication unavailable + 인증 사용 불가 No comment provided by engineer. - + Auto-accept contact requests + 연락처 요청 자동 수락 No comment provided by engineer. - + Auto-accept images + 이미지 자동 수락 No comment provided by engineer. Automatically No comment provided by engineer. - + Back + 뒤로가기 No comment provided by engineer. Both you and your contact can irreversibly delete sent messages. No comment provided by engineer. - + Both you and your contact can send disappearing messages. + 당신과 당신의 연락처 모두 사라지는 메시지를 보낼 수 있습니다. No comment provided by engineer. - + Both you and your contact can send voice messages. + 당신과 당신의 연락처 모두 음성 메시지를 보낼 수 있습니다. No comment provided by engineer. - + By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). + 채팅 프로필(기본값) 또는 [연결](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - + Call already ended! + 통화가 이미 종료되었습니다! No comment provided by engineer. - + Calls + 통화 No comment provided by engineer. Can't delete user profile! No comment provided by engineer. - + Can't invite contact! + 주소를 초대할 수 없습니다. No comment provided by engineer. - + Can't invite contacts! + 연락처를 초대할 수 없습니다! No comment provided by engineer. - + Cancel + 취소 No comment provided by engineer. - + Cannot access keychain to save database password + 데이터베이스 암호를 저장하는 키체인에 접근 할 수 없습니다 No comment provided by engineer. - + Cannot receive file + 파일을 받을 수 없습니다 No comment provided by engineer. - + Change + 변경 No comment provided by engineer. - + Change database passphrase? + 데이터베이스 암호 변경? No comment provided by engineer. - + Change member role? + 멤버 역할을 변경하시겠습니까? No comment provided by engineer. - + Change receiving address + 수신 주소 변경 No comment provided by engineer. Change receiving address? - 修改接收地址? + 수신 주소를 변경하시겠습니까? No comment provided by engineer. - + Change role + 역할 변경 No comment provided by engineer. - + Chat archive + 채팅 기록 보관함 No comment provided by engineer. - + Chat console + 채팅 콘솔 No comment provided by engineer. - + Chat database + 채팅 데이터베이스 No comment provided by engineer. - + Chat database deleted + 채팅 데이터베이스 삭제 No comment provided by engineer. - + Chat database imported + 채팅 데이터베이스를 가져옴 No comment provided by engineer. - + Chat is running + 채팅이 실행 중입니다 No comment provided by engineer. - + Chat is stopped + 채팅이 중단되었습니다 No comment provided by engineer. - + Chat preferences + 채팅 환경설정 No comment provided by engineer. - + Chats + 채팅 No comment provided by engineer. - + Check server address and try again. + 서버 주소를 확인한 후 다시 시도합니다. No comment provided by engineer. - + Chinese and Spanish interface + 중국어 및 스페인어 환경 No comment provided by engineer. - + Choose file + 파일 선택 No comment provided by engineer. - + Choose from library + 라이브러리에서 선택 No comment provided by engineer. - + Clear + 정리 No comment provided by engineer. - + Clear conversation + 대화 삭제 No comment provided by engineer. - + Clear conversation? + 대화 삭제? No comment provided by engineer. - + Clear verification + 인증 삭제 No comment provided by engineer. Colors No comment provided by engineer. - + Compare security codes with your contacts. + 보안 코드를 연락처와 비교합니다. No comment provided by engineer. - + Configure ICE servers + ICE 서버 구성 No comment provided by engineer. - + Confirm + 확인 No comment provided by engineer. - + Confirm new passphrase… + 새 암호 확인… No comment provided by engineer. - + Confirm password + 비밀번호 확인 No comment provided by engineer. - + Connect + 연결 server test step @@ -633,8 +760,9 @@ Connect via group link? No comment provided by engineer. - + Connect via link + 링크를 통해 연결 No comment provided by engineer. @@ -645,174 +773,210 @@ Connect via one-time link? No comment provided by engineer. - + Connecting to server… + 서버에 연결중… No comment provided by engineer. - + Connecting to server… (error: %@) + 서버에 연결중...(오류: %@) No comment provided by engineer. - + Connection + 연결 No comment provided by engineer. - + Connection error + 연결 오류 No comment provided by engineer. - + Connection error (AUTH) + 연결 에러 (인증) No comment provided by engineer. Connection request No comment provided by engineer. - + Connection request sent! + 연결 요청이 전송되었습니다! No comment provided by engineer. - + Connection timeout + 연결 시간초과 No comment provided by engineer. - + Contact allows + 연락 가능 No comment provided by engineer. - + Contact already exists + 연결이 이미 존재 No comment provided by engineer. Contact and all messages will be deleted - this cannot be undone! No comment provided by engineer. - + Contact hidden: + 숨겨진 연락처: notification - + Contact is connected + 연락처가 연결되었습니다 notification Contact is not connected yet! No comment provided by engineer. - + Contact name + 연락처 이름 No comment provided by engineer. - + Contact preferences + 연락처 선호도 No comment provided by engineer. Contact requests No comment provided by engineer. - + Contacts can mark messages for deletion; you will be able to view them. + 연락처는 메세지를 삭제하도록 표시할 수 있으며, 이를 확인할 수 있습니다. No comment provided by engineer. - + Copy + 복사 chat item action Core built at: %@ No comment provided by engineer. - + Core version: v%@ + 코어 버전: v%@ No comment provided by engineer. - + Create + 생성 No comment provided by engineer. Create address No comment provided by engineer. - + Create group link + 그룹 링크 생성 No comment provided by engineer. - + Create link + 링크 생성 No comment provided by engineer. Create one-time invitation link No comment provided by engineer. - + Create queue + 큐 생성 server test step - + Create secret group + 비밀 그룹 생성 No comment provided by engineer. - + Create your profile + 프로필 생성 No comment provided by engineer. Created on %@ No comment provided by engineer. - + Current passphrase… + 현재 암호… No comment provided by engineer. - + Currently maximum supported file size is %@. + 현재 지원되는 최대 파일 크기는 %@입니다. No comment provided by engineer. - + Dark + 다크 No comment provided by engineer. - + Database ID + 데이터베이스 아이디 No comment provided by engineer. - + Database encrypted! + 데이터베이스 암호화됨! No comment provided by engineer. - + Database encryption passphrase will be updated and stored in the keychain. + 데이터베이스 암호화 키가 키체인에 저장됩니다. + No comment provided by engineer. - + Database encryption passphrase will be updated. + 데이터베이스 암호화 키가 업데이트됩니다. + No comment provided by engineer. - + Database error + 데이터베이스 오류 No comment provided by engineer. - + Database is encrypted using a random passphrase, you can change it. + 데이터베이스는 임의의 암호를 사용하여 암호화되므로 변경할 수 있습니다. No comment provided by engineer. - + Database is encrypted using a random passphrase. Please change it before exporting. + 데이터베이스는 임의의 암호를 사용하여 암호화됩니다. 내보내기 전에 변경하십시오. No comment provided by engineer. - + Database passphrase + 데이터베이스 암호화 키 No comment provided by engineer. - + Database passphrase & export + 데이터베이스 암호화 키 & 내보내기 No comment provided by engineer. @@ -1001,8 +1165,8 @@ Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1017,8 +1181,8 @@ 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. @@ -1409,16 +1573,16 @@ Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1525,8 +1689,8 @@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1630,8 +1794,8 @@ 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. @@ -1949,8 +2113,8 @@ We will be adding server redundancy to prevent lost messages. 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**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2001,8 +2165,9 @@ We will be adding server redundancy to prevent lost messages. Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. + 누구나 서버를 호스팅할 수 있습니다. No comment provided by engineer. @@ -2037,8 +2202,8 @@ We will be adding server redundancy to prevent lost messages. Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2397,24 +2562,29 @@ We will be adding server redundancy to prevent lost messages. Send live message No comment provided by engineer. - + Send notifications + 알림 전송 No comment provided by engineer. - + Send notifications: + 알림 전송: No comment provided by engineer. - + Send questions and ideas + 질문이나 아이디어 보내기 No comment provided by engineer. - + Send them from gallery or custom keyboards. + 갤러리 또는 사용자 정의 키보드에서 그들을 보내십시오. No comment provided by engineer. - + Sender cancelled file transfer. + 상대방이 파일 전송을 취소했습니다. No comment provided by engineer. @@ -2653,8 +2823,8 @@ We will be adding server redundancy to prevent lost messages. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -2689,16 +2859,16 @@ We will be adding server redundancy to prevent lost messages. The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of 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. @@ -2757,8 +2927,8 @@ We will be adding server redundancy to prevent lost messages. 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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -2951,8 +3121,8 @@ To connect, please ask your contact to create another connection link and check 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. @@ -3076,10 +3246,6 @@ SimpleX Lock must be enabled. You can't send messages! 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. @@ -3755,6 +3921,1206 @@ SimpleX servers cannot see your profile. \~strike~ No comment provided by engineer. + + Change passcode + 패스코드 변경 + authentication reason + + + Cellular + 셀룰러 + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + 이 서버 또는 도착 서버가 비밀 라우팅을 지원하지 않을 때 직통 메시지 보내기. + No comment provided by engineer. + + + Send up to 100 last messages to new members. + 새로운 멤버에게 최대 100개의 마지막 메시지 보내기. + No comment provided by engineer. + + + ## History + ## 기록 + + + ## In reply to + ## 에 대한 답변 + + + %@ downloaded + %@ 다운로드됨 + + + # %@ + # %@ + + + %@ and %@ + %@ 그리고 %@ + + + %1$@ at %2$@: + %2$@의 %1$@: + + + %@ connected + %@ 연결됨 + + + %@ (current): + %@ (현재): + + + %@ (current) + %@ (현재) + + + %@ and %@ connected + %@ 및 %@이(가) 연결되었습니다 + + + %@ server + %@서버 + + + %@ servers + %@서버들 + + + %@, %@ and %lld members + %@, %@ 과 %lld 멤버들 + + + %d file(s) are still being downloaded. + %d 개의 파일 다운로드중. + + + %d file(s) were deleted. + %d개의 파일이 삭제됨. + + + %d file(s) were not downloaded. + %d개의 파일이 다운로드 되지 않음. + + + %d weeks + %d 주 + + + %lld seconds + %lld 초 + + + **Create 1-time link**: to create and share a new invitation link. + **1회 링크 생성** : 새 초대 링크를 생성하고 공유합니다. + + + 1-time link + 일회성 링크 + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + 일회용 링크는 *한 번의 연락처로만* 사용할 수 있으며, 대면 또는 메신저를 통해 공유할 수 있습니다. + + + A few more things + 몇 가지 더 + + + Accept conditions + 조건 수락 + + + Accepted conditions + 수락된 조건 + + + Active connections + 연결 활성화 + + + %@ uploaded + %@업로드됨 + + + Accept connection request? + 연결 요청을 수락하시겠습니까? + + + %lld minutes + %lld 분 + + + **Warning**: the archive will be removed. + **경고**: 보관물이 제거됩니다. + + + 5 minutes + 5 분 + + + Abort changing address + 주소 변경 중단 + + + Acknowledgement errors + 확인 오류 + + + Abort + 중단 + + + %u messages failed to decrypt. + %u개의 메세지를 번역하는데 실패함. + + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. + 연락처가 다른 사람과 공유할 수 있도록 프로필에 주소를 추가합니다. 프로필 업데이트가 연락처로 전송됩니다. + + + %lld messages blocked by admin + 관리자에 의해 차단된 %lld개의 메세지 + + + **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **참고**: 두 장치에서 동일한 데이터베이스를 사용하면 보안 보호를 위해 연결에서 메세지를 해독할 수 있습니다. + + + **Create group**: to create a new group. + **그룹 생성** : 새로운 그룹을 생성합니다. + + + %d file(s) failed to download. + %d개의 파일을 다운로드하는데 실패함. + + + %d messages not forwarded + %d개의 메세지가 전달되지 않음 + + + **Scan / Paste link**: to connect via a link you received. + **스캔/붙여넣기 링크**: 받은 링크를 통해 연결합니다. + + + About operators + 연산자 정보 + + + Address change will be aborted. Old receiving address will be used. + 주소 변경이 중단됩니다. 이전 수신 주소가 사용됩니다. + + + %@: + %@: + + + %lld messages blocked + %lld개의 메세지가 차단됨 + + + %lld messages marked deleted + 삭제된 메세지 %lld 개 + + + - more stable message delivery. +- a bit better groups. +- and more! + - 보다 안정적인 메세지 전달. +- 조금 더 나은 그룹. +- 그리고 더! + + + 0s + 0초 + + + 1 minute + 1분 + + + Abort changing address? + 주소 변경을 중단하시겠습니까? + + + 30 seconds + 30초 + + + - voice messages up to 5 minutes. +- custom time to disappear. +- editing history. + - 음성 메세지 최대 5분. +- 사라지는 맞춤형 시간. +- 편집 기록. + + + Add friends + 친구 추가 + + + Add team members + 팀원 추가하기 + + + Add your team members to the conversations. + 대화에 팀원을 추가하세요. + + + %u messages skipped. + 메세지 %u개를 건너뜀. + + + %@, %@ and %lld other members connected + %@, %@ 그리고 %lld 다른 멤버들이 연결됨 + + + %lld messages moderated by %@ + %@ 에 의해 중재된 %lld 개의 메세지 + + + %lld new interface languages + %lld개의 새로운 인터페이스 언어 + + + %1$@, %2$@ + %1$@, %2$@ + + + - optionally notify deleted contacts. +- profile names with spaces. +- and more! + - 선택적으로 삭제된 연락처를 통지합니다. +- 공백이 있는 프로필 이름. +- 그리고 더! + + + <p>Hi!</p> +<p><a href="%@">Connect to me via SimpleX Chat</a></p> + <p>안녕하세요!/p> +<p><a href="%@">SimpleX 채팅을 통해 저에게 연결하세요 </a></p> + + + A new random profile will be shared. + 새로운 랜덤 프로필이 공유될 것입니다. + + + Acknowledged + 인정된 + + + Additional accent 2 + 추가 악센트2 + + + Added media & file servers + 미디어 및 파일 서버 추가 + + + Added message servers + 추가된 메세지 서버 + + + Additional accent + 추가 악센트 + + + Additional secondary + 추가적 보조 + + + Address + 주소 + + + Address or 1-time link? + 주소 또는 일회성 링크? + + + Address settings + 주소 세팅 + + + Admins can block a member for all. + 관리자는 모두를 위해 회원을 차단할 수 있습니다. + + + %lld group events + %lld개의 그룹 이벤트 + + + All app data is deleted. + 모든 앱 데이터가 삭제됩니다. + + + All data is erased when it is entered. + 입력하면 모든 데이터가 삭제됩니다. + + + 0 sec + 0 초 + + + (this device v%@) + (이 장치 v%@) + + + (new) + (새로운) + + + Advanced settings + 고급 설정 + + + All data is kept private on your device. + 모든 데이터는 기기에서 비공개로 유지됩니다. + + + All profiles + 전체 프로필 + + + All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + 모든 연락처, 대화 및 파일은 안전하게 암호화되어 구성된 XFTP 릴레이에 청크로 업로드됩니다. + + + Allow calls only if your contact allows them. + 허용된 연락처만 통화가 가능합니다. + + + Allow calls? + 통화 허용? + + + Allow downgrade + 강등 허용 + + + Allow irreversible message deletion only if your contact allows it to you. (24 hours) + 연락처가 허용하는 경우에만 수정 불가능한 메세지 삭제를 허용합니다. (24시간) + + + Allow to send files and media. + 파일과 미디어를 전송할 수 있습니다. + + + Archiving database + 보관된 데이터베이스 + + + Better calls + 더 나은 통화 + + + Block + 차단 + + + Conditions will be accepted for enabled operators after 30 days. + 30일 후에 활성화된 운영자에 대한 조건이 수락될 것입니다. + + + Conditions will be accepted on: %@. + 조건은 다음과 같습니다: %@. + + + Connect via one-time link + 일회성 링크를 통해 연결 + + + Connected desktop + 데스크톱과 연결됨 + + + Connected servers + 연결된 서버 + + + Connection security + 연결 보안 + + + Connection terminated + 종료된 연결 + + + Connection with desktop stopped + 데스크톱과의 연결이 중지됨 + + + Current conditions text couldn't be loaded, you can review conditions via this link: + 현재 조건 텍스트를 로드할 수 없습니다. 이 링크를 통해 조건을 검토할 수 있습니다: + + + Bad desktop address + 잘못된 데스크톱 주소 + + + Camera not available + 카메라가 사용 불가능합니다 + + + Custom time + 사용자 지정 시간 + + + Allow to irreversibly delete sent messages. (24 hours) + 보낸 메시지를 되돌릴 수 없도록 삭제합니다. (24시간) + + + Allow message reactions. + 메세지 응답 허용. + + + Allow your contacts adding message reactions. + 연락처가 메세지 응답을 추가하도록 허용합니다. + + + Already connecting! + 이미 연결 중입니다! + + + Already joining the group! + 그룹에 참가하는 중입니다! + + + Archive and upload + 기록 및 업로드 + + + Chat colors + 채팅 색깔 + + + Chat list + 채팅 목록 + + + Completed + 완료됨 + + + Copy error + 복사 오류 + + + Create SimpleX address + SimpleX 주소 생성 + + + Creating link… + 생성 링크… + + + Blocked by admin + 관리자에 의해 차단됨 + + + Connect to desktop + 데스크톱에 연결 + + + Created at + 에 생성됨 + + + Created at: %@ + 생성 위치: %@ + + + Change self-destruct passcode + 자기-파괴 비밀번호 변경 + + + Create file + 파일 생성 + + + Allow your contacts to irreversibly delete sent messages. (24 hours) + 연락처가 보낸 메세지를 되돌릴 수 없도록 삭제할 수 있도록 허용합니다. (24시간) + + + App data migration + 앱 데이터 이동 + + + Apply to + 적용 대상 + + + Block for all + 모두를 위한 차단 + + + Both you and your contact can add message reactions. + 당신과 당신의 연락처 모두 메세지 반응을 추가할 수 있습니다. + + + Calls prohibited! + 통화 금지! + + + Change self-destruct mode + 자기-파괴 모드 변경 + + + Contacts + 연락처 + + + Create group + 그룹 생성 + + + Both you and your contact can make calls. + 당신과 당신의 연락처 모두 전화를 걸 수 있습니다. + + + App passcode + 앱 비밀번호 + + + All your contacts will remain connected. Profile update will be sent to your contacts. + 당신의 모든 연락은 연결되어 있습니다. 프로필 업데이트가 모든 연락으로 전송됩니다. + + + App encrypts new local files (except videos). + 앱은 새로운 로컬 파일을 암호화합니다 (동영상 제외). + + + Chat preferences were changed. + 채팅 환경설정이 변경되었습니다. + + + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 + [데스크톱 앱]에 새로운 프로필 생성(https://simplex.chat/downloads/).💻 + + + Contact is deleted. + 연락처가 삭제되었습니다. + + + Continue + 계속 + + + Current Passcode + 현재 비밀번호 + + + An empty chat profile with the provided name is created, and the app opens as usual. + 제공된 이름으로 빈 채팅 프로필이 생성되고 앱이 정상적으로 열립니다. + + + Allow your contacts to call you. + 연락처가 전화할 수 있도록 허용합니다. + + + Allow sharing + 공유 허용 + + + Always use private routing. + 항상 개인 경로를 사용합니다. + + + Better user experience + 더 나은 사용자 경험 + + + Change lock mode + 잠금 모드 변경 + + + Allow message reactions only if your contact allows them. + 연락처가 메세지 응답을 허용하는 경우에만 메세지 응답을 허용합니다. + + + Better security ✅ + 더 나은 안전✅ + + + Both you and your contact can irreversibly delete sent messages. (24 hours) + 당신과 당신의 연락처 모두 보낸 메세지를 되돌릴 수 없습니다. (24시간) + + + Confirm contact deletion? + 연락처 삭제를 확인하시겠습니까? + + + Can't call contact + 연락처에 전화할 수 없습니다 + + + 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)에 감사드립니다! + + + Capacity exceeded - recipient did not receive previously sent messages. + 용량 초과 - 수신자가 이전에 보낸 메세지를 받지 못했습니다. + + + Chat + 채팅 + + + Connect to yourself? +This is your own one-time link! + 자신에게 연결할까요? +이것은 당신만의 일회성 링크입니다! + + + Choose _Migrate from another device_ on the new device and scan QR code. + 새 기기에서 _다른 기기에서 이동_을 선택하고 QR 코드를 스캔합니다. + + + Connecting to desktop + 데스크톱에 연결중 + + + Connect with %@ + %@와 연결 + + + Archived contacts + 보관된 연락처 + + + Better message dates. + 더 나은 메세지 날짜. + + + Better networking + 더 나은 네트워킹 + + + Check messages when allowed. + 허용될 때 메시지를 확인합니다. + + + Compare file + 파일 비교 + + + Conditions will be automatically accepted for enabled operators on: %@. + 다음 조건은 활성화된 운영자에 대해 자동으로 수락됩니다: %@. + + + Confirm upload + 업로드 확인 + + + Connect incognito + 비밀 연결 + + + Connect to your friends faster. + 친구들과 더 빨리 연결하세요. + + + Connect to yourself? + 자신과 연결할까요? + + + Created + 생성됨 + + + Creating archive link + 기록 링크 생성하기 + + + Auto-accept + 자동 수락 + + + All new messages from %@ will be hidden! + %@로부터의 모든 새 메세지가 숨겨집니다! + + + Auto-accept settings + 자동-수락 설정 + + + Archive contacts to chat later. + 나중에 채팅할 연락처를 보관합니다. + + + Background + 배경 + + + Bad message hash + 잘못된 메세지 hash + + + Better groups + 더 나은 그룹 + + + Better messages + 더 나은 메세지 + + + Chunks downloaded + 다운로드된 청크 + + + Chunks deleted + 삭제된 청크 + + + Chunks uploaded + 업로드 된 청크 + + + Corner + 코너 + + + Correct name to %@? + %@의 정확한 이름은? + + + Create a group using a random profile. + 랜덤 프로필을 사용하여 그룹을 만듭니다. + + + Authentication cancelled + 인증 취소 + + + Confirm Passcode + 비밀번호 확인 + + + Confirm database upgrades + 데이터베이스 업그레이드 확인 + + + Blur media + 가려진 미디어 + + + Block group members + 그룹 구성원 차단 + + + Connected + 연결됨 + + + All messages will be deleted - this cannot be undone! + 모든 메세지가 삭제됩니다 - 수정할 수 없습니다! + + + All your contacts will remain connected. + 당신의 모든 연락은 계속 연결되어 있습니다. + + + Allow to send SimpleX links. + SinpleX 링크 전송 허용. + + + Bad message ID + 잘못된 메세지 ID + + + Black + 블랙 + + + Block member + 차단 구성원 + + + Connected to desktop + 데스크톱과 연결됨 + + + App passcode is replaced with self-destruct passcode. + 앱 비밀번호는 자체-파괴 비밀번호로 대체됩니다. + + + Apply + 적용 + + + Better notifications + 더 나은 공지 + + + Block member? + 차단 멤버? + + + Blur for better privacy. + 더 나은 개인정보를 위해 흐림. + + + Business address + 사업체 주소 + + + Business chats + 비즈니스 채팅 + + + Can't call member + 회원에게 전화할 수 없습니다 + + + Can't message member + 멤버에게 메세지를 보낼 수 없습니다 + + + Cancel migration + 이동 취소 + + + Change chat profiles + 채팅 프로필 변경 + + + Chat already exists + 채팅이 이미 존재합니다 + + + Chat already exists! + 채팅이 이미 존재합니다! + + + Chat database exported + 채팅 데이터베이스 내보내기 + + + Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. + 채팅이 중지되었습니다. 이미 다른 장치에서 이 데이터베이스를 사용하고 있다면 채팅을 시작하기 전에 다시 전송해야 합니다. + + + Chat migrated! + 채팅 이동! + + + Chat theme + 채팅 테마 + + + Chat will be deleted for all members - this cannot be undone! + 채팅은 모든 회원에게 삭제됩니다 - 이는 되돌릴 수 없습니다! + + + Chat profile + 채팅 프로필 + + + Chat will be deleted for you - this cannot be undone! + 채팅은 삭제됩니다 - 되돌릴 수 없습니다! + + + Check messages every 20 min. + 20분마다 메시지를 확인합니다. + + + Color chats with the new themes. + 새로운 테마로 채팅을 색칠하세요. + + + Color mode + 색깔 모드 + + + Clear private notes? + 개인 메모를 지우시겠습니까? + + + Conditions accepted on: %@. + 조건이 수락됨: %@. + + + Conditions are accepted for the operator(s): **%@**. + 운영자의 조건이 허용됩니다: **%@**. + + + Conditions of use + 이용 조건 + + + Conditions will be accepted for operator(s): **%@**. + 운영자 조건이 수락됩니다: **%@**. + + + Conditions will be accepted for the operator(s): **%@**. + 운영자 조건이 수락됩니다.: **%@**. + + + Confirm that you remember database passphrase to migrate it. + 이동하는데에 필요한 데이터베이스 비밀번호를 기억하는지 확인합니다. + + + Connect to yourself? +This is your own SimpleX address! + 자신과 연결할까요? +이것은 당신의 SimpleX 주소입니다! + + + Connect via contact address + 연락처 주소로 연결 + + + Connecting + 연결중 + + + Connecting to contact, please wait or check later! + 연락처에 연결 중이니 기다려 주시거나 나중에 확인해 주세요! + + + Connection and servers status. + 연결 및 서버 상태. + + + Connection notifications + 연결 공지 + + + Connections + 연결 + + + Contact will be deleted - this cannot be undone! + 연락처가 삭제됩니다 - 취소할 수 없습니다! + + + Conversation deleted! + 대화가 삭제되었습니다! + + + Create 1-time link + 일회성 링크 생성 + + + Create profile + 프로필 생성 + + + Audio/video calls + 음성/영상 통화 + + + Audio/video calls are prohibited. + 음성/영상 통화는 금지되어 있습니다. + + + App session + 앱 세션 + + + Block member for all? + 모두를 위한 차단 멤버? + + + Cannot forward message + 메세지를 전달할 수 없습니다 + + + Confirm network settings + 네트워크 설정 확인 + + + Connect automatically + 자동으로 연결 + + + Confirm files from unknown servers. + 알 수 없는 서버에서 파일을 확인합니다. + + + Conditions are already accepted for these operator(s): **%@**. + 이 운영자들에 대한 조건은 이미 받아들여지고 있습니다: **%@**. + + + Contact deleted! + 연락처 삭제! + + + Current profile + 현재 프로필 + + + Customizable message shape. + 사용자 지정 가능한 메세지 형태. + + + %d seconds(s) + %d 초 + + + 1 year + 1년 + + + Add list + 리스트 추가 + + + Add to list + 리스트에 추가 + + + All + 모두 + + + Allow to report messsages to moderators. + 메시지를 신고하는것을 허용합니다. + + + Another reason + 다른 이유 + + + App group: + 앱 그룹: + + + Archive + 아카이브 + + + Archive report + 신고 아카이브 + + + Archive report? + 신고를 아카이브할까요? + + + Archive reports + 신고 아카이브 + + + Ask + 묻기 + + + Clear group? + 그룹을 비울까요? + + + Clear or delete group? + 그룹을 비우거나 삭제할까요? + + + Community guidelines violation + 커뮤니티 지침 위반 + + + Connection blocked + 연결 차단됨 + + + Connection is blocked by server operator: +%@ + 서버 관리자에 의해 연결이 차단되었습니다: +%@ + + + Connection not ready. + 연결 준비되지 않음. + + + Connection requires encryption renegotiation. + 연결에는 암호화 재협상이 필요합니다. + + + Content violates conditions of use + 내용은 사용 규정을 위반합니다 + + + Create list + 리스트 추가 + + + Database ID: %d + 데이터베이스 아이디: %d + + + Database IDs and Transport isolation option. + 데이터베이스 ID 및 전송 격리 옵션. + + + Database downgrade + 데이터베이스 다운그레이드 + + + Better groups performance + 더 나은 그룹 성능 + + + Confirmed + 확인함 + + + Active + 활성화됨 + + + Archive all reports? + 모든 신고를 아카이브할까요? + + + Businesses + 비즈니스 + + + Better privacy and security + 더 나은 프라이버시 및 보안 + + + Change automatic message deletion? + 자동 메시지 삭제를 변경할까요? + + + All chats will be removed from the list %@, and the list deleted. + 모든 채팅은 %@ 리스트에서 제거되고 리스트는 삭제됩니다. + + + All reports will be archived for you. + 모든 보고서는 사용자를 위해 보관됩니다. + + + Accent + 강조 + + + Archive %lld reports? + %lld 신고를 아카이브할까요? + + + - 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. + - [경로 서비스](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 명의 멤버). +- 더 빠르고 안정적입니다. + + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + 모든 메시지와 파일은 **종간단 암호화 (E2EE)**되며, 개인 메시지는 양자 보안이 적용됩니다. + + + Customize theme + 테마 사용자 지정 + + + Dark mode colors + 다크 모드 색상들 +
@@ -3778,8 +5144,9 @@ SimpleX servers cannot see your profile. SimpleX needs microphone access for audio and video calls, and to record voice messages. Privacy - Microphone Usage Description
- + SimpleX needs access to Photo Library for saving captured and received media + SimpleX는 캡처 및 수신 된 미디어를 저장하기 위해 사진 라이브러리에 접근이 필요합니다 Privacy - Photo Library Additions Usage Description @@ -3793,8 +5160,9 @@ SimpleX servers cannot see your profile. SimpleX NSE Bundle display name - + SimpleX NSE + SimpleX NSE Bundle name 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 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - **Create link / QR code** for your contact to use. 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. + + **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. 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). + + **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. No comment provided by engineer. @@ -187,8 +183,8 @@ **Turėkite omenyje**: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti. 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. + + **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. No comment provided by engineer. @@ -329,9 +325,9 @@ Pridėti serverius skenuojant QR kodus. No comment provided by engineer. - - Add server… - Pridėti serverį… + + Add server + Pridėti serverį No comment provided by engineer. @@ -1033,8 +1029,8 @@ Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1049,8 +1045,8 @@ 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. @@ -1417,16 +1413,16 @@ Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1513,8 +1509,8 @@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1614,8 +1610,8 @@ 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. @@ -1919,8 +1915,8 @@ We will be adding server redundancy to prevent lost messages. 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**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -1971,8 +1967,8 @@ We will be adding server redundancy to prevent lost messages. Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2003,8 +1999,8 @@ We will be adding server redundancy to prevent lost messages. Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2591,8 +2587,8 @@ We will be adding server redundancy to prevent lost messages. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -2627,16 +2623,16 @@ We will be adding server redundancy to prevent lost messages. The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of 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. @@ -2687,8 +2683,8 @@ We will be adding server redundancy to prevent lost messages. 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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -2873,8 +2869,8 @@ To connect, please ask your contact to create another connection link and check 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. @@ -2993,10 +2989,6 @@ To connect, please ask your contact to create another connection link and check You can't send messages! 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. 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 @@ - - - -
- -
- - - - - 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. - - - !1 colored! - No comment provided by engineer. - - - #secret# - No comment provided by engineer. - - - %@ - No comment provided by engineer. - - - %@ %@ - No comment provided by engineer. - - - %@ (current) - No comment provided by engineer. - - - %@ (current): - copied message info - - - %@ / %@ - No comment provided by engineer. - - - %@ is connected! - notification title - - - %@ is not verified - No comment provided by engineer. - - - %@ is verified - No comment provided by engineer. - - - %@ servers - No comment provided by engineer. - - - %@ wants to connect! - notification title - - - %@: - copied message info - - - %d days - time interval - - - %d hours - time interval - - - %d min - time interval - - - %d months - time interval - - - %d sec - time interval - - - %d skipped message(s) - integrity error chat item - - - %d weeks - time interval - - - %lld - No comment provided by engineer. - - - %lld %@ - No comment provided by engineer. - - - %lld contact(s) selected - No comment provided by engineer. - - - %lld file(s) with total size of %@ - No comment provided by engineer. - - - %lld members - No comment provided by engineer. - - - %lld minutes - No comment provided by engineer. - - - %lld second(s) - No comment provided by engineer. - - - %lld seconds - No comment provided by engineer. - - - %lldd - No comment provided by engineer. - - - %lldh - No comment provided by engineer. - - - %lldk - No comment provided by engineer. - - - %lldm - No comment provided by engineer. - - - %lldmth - No comment provided by engineer. - - - %llds - No comment provided by engineer. - - - %lldw - No comment provided by engineer. - - - %u messages failed to decrypt. - No comment provided by engineer. - - - %u messages skipped. - No comment provided by engineer. - - - ( - No comment provided by engineer. - - - ) - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - - - **Create link / QR code** for your contact to use. - 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. - 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). - No comment provided by engineer. - - - **Paste received link** or open it in the browser and tap **Open in mobile app**. - No comment provided by engineer. - - - **Please note**: you will NOT be able to recover or change passphrase if you lose it. - No comment provided by engineer. - - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. - No comment provided by engineer. - - - **Scan QR code**: to connect to your contact in person or via video call. - No comment provided by engineer. - - - **Warning**: Instant push notifications require passphrase saved in Keychain. - No comment provided by engineer. - - - **e2e encrypted** audio call - No comment provided by engineer. - - - **e2e encrypted** video call - No comment provided by engineer. - - - \*bold* - No comment provided by engineer. - - - , - No comment provided by engineer. - - - - voice messages up to 5 minutes. -- custom time to disappear. -- editing history. - No comment provided by engineer. - - - . - No comment provided by engineer. - - - 0s - No comment provided by engineer. - - - 1 day - time interval - - - 1 hour - time interval - - - 1 minute - No comment provided by engineer. - - - 1 month - time interval - - - 1 week - time interval - - - 1-time link - No comment provided by engineer. - - - 5 minutes - No comment provided by engineer. - - - 6 - No comment provided by engineer. - - - 30 seconds - No comment provided by engineer. - - - : - No comment provided by engineer. - - - <p>Hi!</p> -<p><a href="%@">Connect to me via SimpleX Chat</a></p> - email text - - - A new contact - notification title - - - A random profile will be sent to the contact that you received this link from - No comment provided by engineer. - - - A random profile will be sent to your contact - No comment provided by engineer. - - - A separate TCP connection will be used **for each chat profile you have in the app**. - No comment provided by engineer. - - - 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. - No comment provided by engineer. - - - About SimpleX - No comment provided by engineer. - - - About SimpleX Chat - No comment provided by engineer. - - - About SimpleX address - No comment provided by engineer. - - - Accent color - No comment provided by engineer. - - - Accept - accept contact request via notification - accept incoming call via notification - - - Accept contact - No comment provided by engineer. - - - Accept contact request from %@? - notification body - - - Accept incognito - 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 servers by scanning QR codes. - No comment provided by engineer. - - - Add server… - No comment provided by engineer. - - - Add to another device - No comment provided by engineer. - - - Add welcome message - No comment provided by engineer. - - - Address - No comment provided by engineer. - - - Admins can create the links to join groups. - No comment provided by engineer. - - - Advanced network settings - No comment provided by engineer. - - - All app data is deleted. - No comment provided by engineer. - - - All chats and messages will be deleted - this cannot be undone! - No comment provided by engineer. - - - All data is erased when it is entered. - No comment provided by engineer. - - - All group members will remain connected. - No comment provided by engineer. - - - All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - No comment provided by engineer. - - - All your contacts will remain connected. - No comment provided by engineer. - - - All your contacts will remain connected. Profile update will be sent to your contacts. - No comment provided by engineer. - - - Allow - No comment provided by engineer. - - - Allow calls only if your contact allows them. - No comment provided by engineer. - - - Allow disappearing messages only if your contact allows it to you. - No comment provided by engineer. - - - Allow irreversible message deletion only if your contact allows it to you. - No comment provided by engineer. - - - Allow message reactions only if your contact allows them. - No comment provided by engineer. - - - Allow message reactions. - No comment provided by engineer. - - - Allow sending direct messages to members. - No comment provided by engineer. - - - Allow sending disappearing messages. - No comment provided by engineer. - - - Allow to irreversibly delete sent messages. - No comment provided by engineer. - - - Allow to send voice messages. - No comment provided by engineer. - - - Allow voice messages only if your contact allows them. - No comment provided by engineer. - - - Allow voice messages? - No comment provided by engineer. - - - Allow your contacts adding message reactions. - No comment provided by engineer. - - - Allow your contacts to call you. - No comment provided by engineer. - - - Allow your contacts to irreversibly delete sent messages. - No comment provided by engineer. - - - Allow your contacts to send disappearing messages. - No comment provided by engineer. - - - Allow your contacts to send voice messages. - No comment provided by engineer. - - - Already connected? - No comment provided by engineer. - - - Always use relay - No comment provided by engineer. - - - An empty chat profile with the provided name is created, and the app opens as usual. - No comment provided by engineer. - - - Answer call - No comment provided by engineer. - - - App build: %@ - No comment provided by engineer. - - - App icon - No comment provided by engineer. - - - App passcode - No comment provided by engineer. - - - App passcode is replaced with self-destruct passcode. - No comment provided by engineer. - - - App version - No comment provided by engineer. - - - App version: v%@ - No comment provided by engineer. - - - Appearance - No comment provided by engineer. - - - Attach - No comment provided by engineer. - - - Audio & video calls - No comment provided by engineer. - - - Audio and video calls - No comment provided by engineer. - - - Audio/video calls - chat feature - - - Audio/video calls are prohibited. - No comment provided by engineer. - - - Authentication cancelled - PIN entry - - - Authentication failed - No comment provided by engineer. - - - Authentication is required before the call is connected, but you may miss calls. - No comment provided by engineer. - - - Authentication unavailable - No comment provided by engineer. - - - Auto-accept - No comment provided by engineer. - - - Auto-accept contact requests - No comment provided by engineer. - - - Auto-accept images - No comment provided by engineer. - - - Back - No comment provided by engineer. - - - Bad message ID - No comment provided by engineer. - - - Bad message hash - No comment provided by engineer. - - - Better messages - No comment provided by engineer. - - - Both you and your contact can add message reactions. - No comment provided by engineer. - - - Both you and your contact can irreversibly delete sent messages. - No comment provided by engineer. - - - Both you and your contact can make calls. - No comment provided by engineer. - - - Both you and your contact can send disappearing messages. - No comment provided by engineer. - - - Both you and your contact can send voice messages. - No comment provided by engineer. - - - By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). - No comment provided by engineer. - - - Call already ended! - No comment provided by engineer. - - - Calls - No comment provided by engineer. - - - Can't delete user profile! - No comment provided by engineer. - - - Can't invite contact! - No comment provided by engineer. - - - Can't invite contacts! - No comment provided by engineer. - - - Cancel - No comment provided by engineer. - - - Cannot access keychain to save database password - No comment provided by engineer. - - - Cannot receive file - No comment provided by engineer. - - - Change - No comment provided by engineer. - - - Change database passphrase? - No comment provided by engineer. - - - Change lock mode - authentication reason - - - Change member role? - No comment provided by engineer. - - - Change passcode - authentication reason - - - Change receiving address - No comment provided by engineer. - - - Change receiving address? - No comment provided by engineer. - - - Change role - No comment provided by engineer. - - - Change self-destruct mode - authentication reason - - - Change self-destruct passcode - authentication reason - set passcode view - - - Chat archive - No comment provided by engineer. - - - Chat console - No comment provided by engineer. - - - Chat database - No comment provided by engineer. - - - Chat database deleted - No comment provided by engineer. - - - Chat database imported - No comment provided by engineer. - - - Chat is running - No comment provided by engineer. - - - Chat is stopped - No comment provided by engineer. - - - Chat preferences - No comment provided by engineer. - - - Chats - No comment provided by engineer. - - - Check server address and try again. - No comment provided by engineer. - - - Chinese and Spanish interface - No comment provided by engineer. - - - Choose file - No comment provided by engineer. - - - Choose from library - No comment provided by engineer. - - - Clear - No comment provided by engineer. - - - Clear conversation - No comment provided by engineer. - - - Clear conversation? - No comment provided by engineer. - - - Clear verification - No comment provided by engineer. - - - Colors - No comment provided by engineer. - - - Compare file - server test step - - - Compare security codes with your contacts. - No comment provided by engineer. - - - Configure ICE servers - No comment provided by engineer. - - - Confirm - No comment provided by engineer. - - - Confirm Passcode - No comment provided by engineer. - - - Confirm database upgrades - No comment provided by engineer. - - - Confirm new passphrase… - No comment provided by engineer. - - - Confirm password - No comment provided by engineer. - - - Connect - server test step - - - Connect via contact link? - No comment provided by engineer. - - - Connect via group link? - No comment provided by engineer. - - - Connect via link - No comment provided by engineer. - - - Connect via link / QR code - No comment provided by engineer. - - - Connect via one-time link? - No comment provided by engineer. - - - Connecting to server… - No comment provided by engineer. - - - Connecting to server… (error: %@) - No comment provided by engineer. - - - Connection - No comment provided by engineer. - - - Connection error - No comment provided by engineer. - - - Connection error (AUTH) - No comment provided by engineer. - - - Connection request - No comment provided by engineer. - - - Connection request sent! - No comment provided by engineer. - - - Connection timeout - No comment provided by engineer. - - - Contact allows - No comment provided by engineer. - - - Contact already exists - No comment provided by engineer. - - - Contact and all messages will be deleted - this cannot be undone! - No comment provided by engineer. - - - Contact hidden: - notification - - - Contact is connected - notification - - - Contact is not connected yet! - No comment provided by engineer. - - - Contact name - No comment provided by engineer. - - - Contact preferences - No comment provided by engineer. - - - Contacts can mark messages for deletion; you will be able to view them. - No comment provided by engineer. - - - Continue - No comment provided by engineer. - - - Copy - chat item action - - - Core version: v%@ - No comment provided by engineer. - - - Create - No comment provided by engineer. - - - Create SimpleX address - No comment provided by engineer. - - - Create an address to let people connect with you. - No comment provided by engineer. - - - Create file - server test step - - - Create group link - No comment provided by engineer. - - - Create link - No comment provided by engineer. - - - Create one-time invitation link - No comment provided by engineer. - - - Create queue - server test step - - - Create secret group - No comment provided by engineer. - - - Create your profile - No comment provided by engineer. - - - Created on %@ - No comment provided by engineer. - - - Current Passcode - No comment provided by engineer. - - - Current passphrase… - No comment provided by engineer. - - - Currently maximum supported file size is %@. - No comment provided by engineer. - - - Custom time - No comment provided by engineer. - - - Dark - No comment provided by engineer. - - - Database ID - No comment provided by engineer. - - - Database ID: %d - copied message info - - - Database IDs and Transport isolation option. - No comment provided by engineer. - - - Database downgrade - No comment provided by engineer. - - - Database encrypted! - No comment provided by engineer. - - - Database encryption passphrase will be updated and stored in the keychain. - - No comment provided by engineer. - - - Database encryption passphrase will be updated. - - No comment provided by engineer. - - - Database error - No comment provided by engineer. - - - Database is encrypted using a random passphrase, you can change it. - No comment provided by engineer. - - - Database is encrypted using a random passphrase. Please change it before exporting. - No comment provided by engineer. - - - Database passphrase - No comment provided by engineer. - - - Database passphrase & export - No comment provided by engineer. - - - Database passphrase is different from saved in the keychain. - No comment provided by engineer. - - - Database passphrase is required to open chat. - No comment provided by engineer. - - - Database upgrade - No comment provided by engineer. - - - Database will be encrypted and the passphrase stored in the keychain. - - No comment provided by engineer. - - - Database will be encrypted. - - No comment provided by engineer. - - - Database will be migrated when the app restarts - No comment provided by engineer. - - - Decentralized - No comment provided by engineer. - - - Decryption error - No comment provided by engineer. - - - Delete - chat item action - - - Delete Contact - No comment provided by engineer. - - - Delete address - No comment provided by engineer. - - - Delete address? - No comment provided by engineer. - - - Delete after - No comment provided by engineer. - - - Delete all files - No comment provided by engineer. - - - Delete archive - No comment provided by engineer. - - - Delete chat archive? - No comment provided by engineer. - - - Delete chat profile - No comment provided by engineer. - - - Delete chat profile? - No comment provided by engineer. - - - Delete connection - No comment provided by engineer. - - - Delete contact - No comment provided by engineer. - - - Delete contact? - No comment provided by engineer. - - - Delete database - No comment provided by engineer. - - - Delete file - server test step - - - Delete files and media? - No comment provided by engineer. - - - Delete files for all chat profiles - No comment provided by engineer. - - - Delete for everyone - chat feature - - - Delete for me - No comment provided by engineer. - - - Delete group - No comment provided by engineer. - - - Delete group? - No comment provided by engineer. - - - Delete invitation - No comment provided by engineer. - - - Delete link - No comment provided by engineer. - - - Delete link? - No comment provided by engineer. - - - Delete member message? - No comment provided by engineer. - - - Delete message? - No comment provided by engineer. - - - Delete messages - No comment provided by engineer. - - - Delete messages after - No comment provided by engineer. - - - Delete old database - No comment provided by engineer. - - - Delete old database? - No comment provided by engineer. - - - Delete pending connection - No comment provided by engineer. - - - Delete pending connection? - No comment provided by engineer. - - - Delete profile - No comment provided by engineer. - - - Delete queue - server test step - - - Delete user profile? - No comment provided by engineer. - - - Deleted at - No comment provided by engineer. - - - Deleted at: %@ - copied message info - - - Description - No comment provided by engineer. - - - Develop - No comment provided by engineer. - - - Developer tools - No comment provided by engineer. - - - Device - No comment provided by engineer. - - - Device authentication is disabled. Turning off SimpleX Lock. - No comment provided by engineer. - - - Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. - No comment provided by engineer. - - - Different names, avatars and transport isolation. - No comment provided by engineer. - - - Direct messages - chat feature - - - Direct messages between members are prohibited in this group. - No comment provided by engineer. - - - Disable SimpleX Lock - authentication reason - - - Disappearing message - No comment provided by engineer. - - - Disappearing messages - chat feature - - - Disappearing messages are prohibited in this chat. - No comment provided by engineer. - - - Disappearing messages are prohibited in this group. - No comment provided by engineer. - - - Disappears at - No comment provided by engineer. - - - Disappears at: %@ - copied message info - - - Disconnect - server test step - - - Display name - No comment provided by engineer. - - - Display name: - No comment provided by engineer. - - - Do NOT use SimpleX for emergency calls. - No comment provided by engineer. - - - Do it later - No comment provided by engineer. - - - Don't create address - No comment provided by engineer. - - - Don't show again - No comment provided by engineer. - - - Downgrade and open chat - No comment provided by engineer. - - - Download file - server test step - - - Duplicate display name! - No comment provided by engineer. - - - Duration - No comment provided by engineer. - - - Edit - chat item action - - - Edit group profile - No comment provided by engineer. - - - Enable - No comment provided by engineer. - - - Enable SimpleX Lock - authentication reason - - - Enable TCP keep-alive - No comment provided by engineer. - - - Enable automatic message deletion? - No comment provided by engineer. - - - Enable instant notifications? - No comment provided by engineer. - - - Enable lock - No comment provided by engineer. - - - Enable notifications - No comment provided by engineer. - - - Enable periodic notifications? - No comment provided by engineer. - - - Enable self-destruct - No comment provided by engineer. - - - Enable self-destruct passcode - set passcode view - - - Encrypt - No comment provided by engineer. - - - Encrypt database? - No comment provided by engineer. - - - Encrypted database - No comment provided by engineer. - - - Encrypted message or another event - notification - - - Encrypted message: database error - notification - - - Encrypted message: database migration error - notification - - - Encrypted message: keychain error - notification - - - Encrypted message: no passphrase - notification - - - Encrypted message: unexpected error - notification - - - Enter Passcode - No comment provided by engineer. - - - Enter correct passphrase. - No comment provided by engineer. - - - Enter passphrase… - No comment provided by engineer. - - - Enter password above to show! - No comment provided by engineer. - - - Enter server manually - No comment provided by engineer. - - - Enter welcome message… - placeholder - - - Enter welcome message… (optional) - placeholder - - - Error - No comment provided by engineer. - - - Error accepting contact request - No comment provided by engineer. - - - Error accessing database file - No comment provided by engineer. - - - Error adding member(s) - No comment provided by engineer. - - - Error changing address - No comment provided by engineer. - - - Error changing role - No comment provided by engineer. - - - Error changing setting - No comment provided by engineer. - - - Error creating address - No comment provided by engineer. - - - Error creating group - No comment provided by engineer. - - - Error creating group link - No comment provided by engineer. - - - Error creating profile! - No comment provided by engineer. - - - Error deleting chat database - No comment provided by engineer. - - - Error deleting chat! - No comment provided by engineer. - - - Error deleting connection - No comment provided by engineer. - - - Error deleting contact - No comment provided by engineer. - - - Error deleting database - No comment provided by engineer. - - - Error deleting old database - No comment provided by engineer. - - - Error deleting token - No comment provided by engineer. - - - Error deleting user profile - No comment provided by engineer. - - - Error enabling notifications - No comment provided by engineer. - - - Error encrypting database - No comment provided by engineer. - - - Error exporting chat database - No comment provided by engineer. - - - Error importing chat database - No comment provided by engineer. - - - Error joining group - No comment provided by engineer. - - - Error loading %@ servers - No comment provided by engineer. - - - Error receiving file - No comment provided by engineer. - - - Error removing member - No comment provided by engineer. - - - Error saving %@ servers - No comment provided by engineer. - - - Error saving ICE servers - No comment provided by engineer. - - - Error saving group profile - No comment provided by engineer. - - - Error saving passcode - No comment provided by engineer. - - - Error saving passphrase to keychain - No comment provided by engineer. - - - Error saving user password - No comment provided by engineer. - - - Error sending email - No comment provided by engineer. - - - Error sending message - No comment provided by engineer. - - - Error starting chat - No comment provided by engineer. - - - Error stopping chat - No comment provided by engineer. - - - Error switching profile! - No comment provided by engineer. - - - Error updating group link - No comment provided by engineer. - - - Error updating message - No comment provided by engineer. - - - Error updating settings - No comment provided by engineer. - - - Error updating user privacy - No comment provided by engineer. - - - Error: - No comment provided by engineer. - - - Error: %@ - No comment provided by engineer. - - - Error: URL is invalid - No comment provided by engineer. - - - Error: no database file - No comment provided by engineer. - - - Exit without saving - No comment provided by engineer. - - - Export database - No comment provided by engineer. - - - Export error: - No comment provided by engineer. - - - Exported database archive. - No comment provided by engineer. - - - Exporting database archive... - No comment provided by engineer. - - - Failed to remove passphrase - No comment provided by engineer. - - - Fast and no wait until the sender is online! - No comment provided by engineer. - - - File will be deleted from servers. - No comment provided by engineer. - - - File will be received when your contact completes uploading it. - No comment provided by engineer. - - - File will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - File: %@ - No comment provided by engineer. - - - Files & media - No comment provided by engineer. - - - Finally, we have them! 🚀 - No comment provided by engineer. - - - For console - No comment provided by engineer. - - - French interface - No comment provided by engineer. - - - Full link - No comment provided by engineer. - - - Full name (optional) - No comment provided by engineer. - - - Full name: - No comment provided by engineer. - - - Fully re-implemented - work in background! - No comment provided by engineer. - - - Further reduced battery usage - No comment provided by engineer. - - - GIFs and stickers - No comment provided by engineer. - - - Group - No comment provided by engineer. - - - Group display name - No comment provided by engineer. - - - Group full name (optional) - No comment provided by engineer. - - - Group image - No comment provided by engineer. - - - Group invitation - No comment provided by engineer. - - - Group invitation expired - No comment provided by engineer. - - - Group invitation is no longer valid, it was removed by sender. - No comment provided by engineer. - - - Group link - No comment provided by engineer. - - - Group links - No comment provided by engineer. - - - Group members can add message reactions. - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. - No comment provided by engineer. - - - Group members can send direct messages. - No comment provided by engineer. - - - Group members can send disappearing messages. - No comment provided by engineer. - - - Group members can send voice messages. - No comment provided by engineer. - - - Group message: - notification - - - Group moderation - No comment provided by engineer. - - - Group preferences - No comment provided by engineer. - - - Group profile - No comment provided by engineer. - - - Group profile is stored on members' devices, not on the servers. - No comment provided by engineer. - - - Group welcome message - No comment provided by engineer. - - - Group will be deleted for all members - this cannot be undone! - No comment provided by engineer. - - - Group will be deleted for you - this cannot be undone! - No comment provided by engineer. - - - Help - No comment provided by engineer. - - - Hidden - No comment provided by engineer. - - - Hidden chat profiles - No comment provided by engineer. - - - Hidden profile password - No comment provided by engineer. - - - Hide - chat item action - - - Hide app screen in the recent apps. - No comment provided by engineer. - - - Hide profile - No comment provided by engineer. - - - Hide: - No comment provided by engineer. - - - History - copied message info - - - How SimpleX works - No comment provided by engineer. - - - How it works - No comment provided by engineer. - - - How to - No comment provided by engineer. - - - How to use it - No comment provided by engineer. - - - How to use your servers - No comment provided by engineer. - - - ICE servers (one per line) - No comment provided by engineer. - - - If you can't meet in person, show QR code in a video call, or share the link. - No comment provided by engineer. - - - If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link. - No comment provided by engineer. - - - If you enter this passcode when opening the app, all app data will be irreversibly removed! - No comment provided by engineer. - - - If you enter your self-destruct passcode while opening the app: - No comment provided by engineer. - - - If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). - No comment provided by engineer. - - - Ignore - No comment provided by engineer. - - - Image will be received when your contact completes uploading it. - No comment provided by engineer. - - - Image will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - Immediately - No comment provided by engineer. - - - Immune to spam and abuse - No comment provided by engineer. - - - Import - No comment provided by engineer. - - - Import chat database? - No comment provided by engineer. - - - Import database - No comment provided by engineer. - - - Improved privacy and security - No comment provided by engineer. - - - Improved server configuration - No comment provided by engineer. - - - Incognito - No comment provided by engineer. - - - Incognito mode - No comment provided by engineer. - - - Incognito mode is not supported here - your main profile will be sent to group members - No comment provided by engineer. - - - Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created. - No comment provided by engineer. - - - Incoming audio call - notification - - - Incoming call - notification - - - Incoming video call - notification - - - Incompatible database version - No comment provided by engineer. - - - Incorrect passcode - PIN entry - - - Incorrect security code! - No comment provided by engineer. - - - Info - chat item action - - - Initial role - No comment provided by engineer. - - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - - - Instant push notifications will be hidden! - - No comment provided by engineer. - - - Instantly - No comment provided by engineer. - - - Interface - No comment provided by engineer. - - - Invalid connection link - No comment provided by engineer. - - - Invalid server address! - No comment provided by engineer. - - - Invitation expired! - No comment provided by engineer. - - - Invite friends - No comment provided by engineer. - - - Invite members - No comment provided by engineer. - - - Invite to group - No comment provided by engineer. - - - Irreversible message deletion - No comment provided by engineer. - - - Irreversible message deletion is prohibited in this chat. - No comment provided by engineer. - - - Irreversible message deletion is prohibited in this group. - No comment provided by engineer. - - - It allows having many anonymous connections without any shared data between them in a single chat profile. - No comment provided by engineer. - - - It can happen when you or your connection used the old database backup. - No comment provided by engineer. - - - It can happen when: -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. - No comment provided by engineer. - - - It seems like you are already connected via this link. If it is not the case, there was an error (%@). - No comment provided by engineer. - - - Italian interface - No comment provided by engineer. - - - Japanese interface - No comment provided by engineer. - - - Join - No comment provided by engineer. - - - Join group - No comment provided by engineer. - - - Join incognito - No comment provided by engineer. - - - Joining group - No comment provided by engineer. - - - KeyChain error - No comment provided by engineer. - - - Keychain error - No comment provided by engineer. - - - LIVE - No comment provided by engineer. - - - Large file! - No comment provided by engineer. - - - Learn more - No comment provided by engineer. - - - Leave - No comment provided by engineer. - - - Leave group - No comment provided by engineer. - - - Leave group? - No comment provided by engineer. - - - Let's talk in SimpleX Chat - email subject - - - Light - No comment provided by engineer. - - - Limitations - No comment provided by engineer. - - - Live message! - No comment provided by engineer. - - - Live messages - No comment provided by engineer. - - - Local name - No comment provided by engineer. - - - Local profile data only - No comment provided by engineer. - - - Lock after - No comment provided by engineer. - - - Lock mode - No comment provided by engineer. - - - Make a private connection - No comment provided by engineer. - - - Make 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. - No comment provided by engineer. - - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - No comment provided by engineer. - - - Mark deleted for everyone - No comment provided by engineer. - - - Mark read - No comment provided by engineer. - - - Mark verified - No comment provided by engineer. - - - Markdown in messages - No comment provided by engineer. - - - Max 30 seconds, received instantly. - No comment provided by engineer. - - - Member - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - No comment provided by engineer. - - - Member will be removed from group - this cannot be undone! - No comment provided by engineer. - - - Message delivery error - No comment provided by engineer. - - - Message draft - No comment provided by engineer. - - - Message reactions - chat feature - - - Message reactions are prohibited in this chat. - No comment provided by engineer. - - - Message reactions are prohibited in this group. - No comment provided by engineer. - - - Message text - No comment provided by engineer. - - - Messages - No comment provided by engineer. - - - Messages & files - No comment provided by engineer. - - - Migrating database archive... - No comment provided by engineer. - - - Migration error: - No comment provided by engineer. - - - Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). - No comment provided by engineer. - - - Migration is completed - No comment provided by engineer. - - - Migrations: %@ - No comment provided by engineer. - - - Moderate - chat item action - - - Moderated at - No comment provided by engineer. - - - Moderated at: %@ - copied message info - - - More improvements are coming soon! - No comment provided by engineer. - - - Most likely this contact has deleted the connection with you. - No comment provided by engineer. - - - Multiple chat profiles - No comment provided by engineer. - - - Mute - No comment provided by engineer. - - - Muted when inactive! - No comment provided by engineer. - - - Name - No comment provided by engineer. - - - Network & servers - No comment provided by engineer. - - - Network settings - No comment provided by engineer. - - - Network status - No comment provided by engineer. - - - New Passcode - No comment provided by engineer. - - - New contact request - notification - - - New contact: - notification - - - New database archive - No comment provided by engineer. - - - New display name - No comment provided by engineer. - - - New in %@ - No comment provided by engineer. - - - New member role - No comment provided by engineer. - - - New message - notification - - - New passphrase… - No comment provided by engineer. - - - No - No comment provided by engineer. - - - No app password - Authentication unavailable - - - No contacts selected - No comment provided by engineer. - - - No contacts to add - No comment provided by engineer. - - - No device token! - No comment provided by engineer. - - - Group not found! - No comment provided by engineer. - - - No permission to record voice message - No comment provided by engineer. - - - No received or sent files - No comment provided by engineer. - - - Notifications - No comment provided by engineer. - - - Notifications are disabled! - No comment provided by engineer. - - - Now admins can: -- delete members' messages. -- disable members ("observer" role) - No comment provided by engineer. - - - Off - No comment provided by engineer. - - - Off (Local) - No comment provided by engineer. - - - Ok - No comment provided by engineer. - - - Old database - No comment provided by engineer. - - - Old database archive - No comment provided by engineer. - - - One-time invitation link - No comment provided by engineer. - - - Onion hosts will be required for connection. Requires enabling VPN. - No comment provided by engineer. - - - Onion hosts will be used when available. Requires enabling 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**. - No comment provided by engineer. - - - Only group owners can change group preferences. - No comment provided by engineer. - - - Only group owners can enable voice messages. - No comment provided by engineer. - - - Only you can add message reactions. - No comment provided by engineer. - - - Only you can irreversibly delete messages (your contact can mark them for deletion). - No comment provided by engineer. - - - Only you can make calls. - No comment provided by engineer. - - - Only you can send disappearing messages. - No comment provided by engineer. - - - Only you can send voice messages. - No comment provided by engineer. - - - Only your contact can add message reactions. - No comment provided by engineer. - - - Only your contact can irreversibly delete messages (you can mark them for deletion). - No comment provided by engineer. - - - Only your contact can make calls. - No comment provided by engineer. - - - Only your contact can send disappearing messages. - No comment provided by engineer. - - - Only your contact can send voice messages. - No comment provided by engineer. - - - Open Settings - No comment provided by engineer. - - - Open chat - No comment provided by engineer. - - - Open chat console - authentication reason - - - Open user profiles - authentication reason - - - Open-source protocol and code – anybody can run the servers. - No comment provided by engineer. - - - Opening database… - No comment provided by engineer. - - - Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red. - No comment provided by engineer. - - - PING count - No comment provided by engineer. - - - PING interval - No comment provided by engineer. - - - Passcode - No comment provided by engineer. - - - Passcode changed! - No comment provided by engineer. - - - Passcode entry - No comment provided by engineer. - - - Passcode not changed! - No comment provided by engineer. - - - Passcode set! - No comment provided by engineer. - - - Password to show - No comment provided by engineer. - - - Paste - No comment provided by engineer. - - - Paste image - No comment provided by engineer. - - - Paste received link - No comment provided by engineer. - - - Paste the link you received into the box below to connect with your contact. - No comment provided by engineer. - - - People can connect to you only via the links you share. - No comment provided by engineer. - - - Periodically - No comment provided by engineer. - - - Permanent decryption error - message decrypt error item - - - Please ask your contact to enable sending voice messages. - No comment provided by engineer. - - - Please check that you used the correct link or ask your contact to send you another one. - No comment provided by engineer. - - - Please check your network connection with %@ and try again. - No comment provided by engineer. - - - Please check yours and your contact preferences. - No comment provided by engineer. - - - Please contact group admin. - No comment provided by engineer. - - - Please enter correct current passphrase. - No comment provided by engineer. - - - Please enter the previous password after restoring database backup. This action can not be undone. - No comment provided by engineer. - - - Please remember or store it securely - there is no way to recover a lost passcode! - No comment provided by engineer. - - - Please report it to the developers. - No comment provided by engineer. - - - Please restart the app and migrate the database to enable push notifications. - No comment provided by engineer. - - - Please store passphrase securely, you will NOT be able to access chat if you lose it. - No comment provided by engineer. - - - Please store passphrase securely, you will NOT be able to change it if you lose it. - No comment provided by engineer. - - - Polish interface - No comment provided by engineer. - - - Possibly, certificate fingerprint in server address is incorrect - server test error - - - Preserve the last message draft, with attachments. - No comment provided by engineer. - - - Preset server - No comment provided by engineer. - - - Preset server address - No comment provided by engineer. - - - Preview - No comment provided by engineer. - - - Privacy & security - No comment provided by engineer. - - - Privacy redefined - No comment provided by engineer. - - - Private filenames - No comment provided by engineer. - - - Profile and server connections - No comment provided by engineer. - - - Profile image - No comment provided by engineer. - - - Profile password - No comment provided by engineer. - - - Profile update will be sent to your contacts. - No comment provided by engineer. - - - Prohibit audio/video calls. - No comment provided by engineer. - - - Prohibit irreversible message deletion. - No comment provided by engineer. - - - Prohibit message reactions. - No comment provided by engineer. - - - Prohibit messages reactions. - No comment provided by engineer. - - - Prohibit sending direct messages to members. - No comment provided by engineer. - - - Prohibit sending disappearing messages. - No comment provided by engineer. - - - Prohibit sending voice messages. - No comment provided by engineer. - - - Protect app screen - No comment provided by engineer. - - - Protect your chat profiles with a password! - No comment provided by engineer. - - - Protocol timeout - No comment provided by engineer. - - - Push notifications - No comment provided by engineer. - - - Rate the app - No comment provided by engineer. - - - React... - chat item menu - - - 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). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - No comment provided by engineer. - - - Read more in our GitHub repository. - No comment provided by engineer. - - - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - No comment provided by engineer. - - - Received at - No comment provided by engineer. - - - Received at: %@ - copied message info - - - Received file event - notification - - - Received message - message info title - - - Receiving file will be stopped. - No comment provided by engineer. - - - Receiving via - No comment provided by engineer. - - - Recipients see updates as you type them. - No comment provided by engineer. - - - Record updated at - No comment provided by engineer. - - - Record updated at: %@ - copied message info - - - Reduced battery usage - No comment provided by engineer. - - - Reject - reject incoming call via notification - - - Reject contact (sender NOT notified) - No comment provided by engineer. - - - Reject contact request - No comment provided by engineer. - - - Relay server is only used if necessary. Another party can observe your IP address. - No comment provided by engineer. - - - Relay server protects your IP address, but it can observe the duration of the call. - No comment provided by engineer. - - - Remove - No comment provided by engineer. - - - Remove member - No comment provided by engineer. - - - Remove member? - No comment provided by engineer. - - - Remove passphrase from keychain? - No comment provided by engineer. - - - Reply - chat item action - - - Required - No comment provided by engineer. - - - Reset - No comment provided by engineer. - - - Reset colors - No comment provided by engineer. - - - Reset to defaults - No comment provided by engineer. - - - Restart the app to create a new chat profile - No comment provided by engineer. - - - Restart the app to use imported chat database - No comment provided by engineer. - - - Restore - No comment provided by engineer. - - - Restore database backup - No comment provided by engineer. - - - Restore database backup? - No comment provided by engineer. - - - Restore database error - No comment provided by engineer. - - - Reveal - chat item action - - - Revert - No comment provided by engineer. - - - Revoke - No comment provided by engineer. - - - Revoke file - cancel file action - - - Revoke file? - No comment provided by engineer. - - - Role - No comment provided by engineer. - - - Run chat - No comment provided by engineer. - - - SMP servers - No comment provided by engineer. - - - Save - chat item action - - - Save (and notify contacts) - No comment provided by engineer. - - - Save and notify contact - No comment provided by engineer. - - - Save and notify group members - No comment provided by engineer. - - - Save and update group profile - No comment provided by engineer. - - - Save archive - No comment provided by engineer. - - - Save auto-accept settings - No comment provided by engineer. - - - Save group profile - No comment provided by engineer. - - - Save passphrase and open chat - No comment provided by engineer. - - - Save passphrase in Keychain - No comment provided by engineer. - - - Save preferences? - No comment provided by engineer. - - - Save profile password - No comment provided by engineer. - - - Save servers - No comment provided by engineer. - - - Save servers? - No comment provided by engineer. - - - Save settings? - No comment provided by engineer. - - - Save welcome message? - No comment provided by engineer. - - - Saved WebRTC ICE servers will be removed - No comment provided by engineer. - - - Scan QR code - No comment provided by engineer. - - - Scan code - No comment provided by engineer. - - - Scan security code from your contact's app. - No comment provided by engineer. - - - Scan server QR code - No comment provided by engineer. - - - Search - No comment provided by engineer. - - - Secure queue - server test step - - - Security assessment - No comment provided by engineer. - - - Security code - No comment provided by engineer. - - - Select - No comment provided by engineer. - - - Self-destruct - No comment provided by engineer. - - - Self-destruct passcode - No comment provided by engineer. - - - Self-destruct passcode changed! - No comment provided by engineer. - - - Self-destruct passcode enabled! - No comment provided by engineer. - - - Send - No comment provided by engineer. - - - Send a live message - it will update for the recipient(s) as you type it - No comment provided by engineer. - - - Send direct message - No comment provided by engineer. - - - Send disappearing message - No comment provided by engineer. - - - Send link previews - No comment provided by engineer. - - - Send live message - No comment provided by engineer. - - - Send notifications - No comment provided by engineer. - - - Send notifications: - No comment provided by engineer. - - - Send questions and ideas - No comment provided by engineer. - - - Send them from gallery or custom keyboards. - No comment provided by engineer. - - - Sender cancelled file transfer. - No comment provided by engineer. - - - Sender may have deleted the connection request. - No comment provided by engineer. - - - Sending file will be stopped. - No comment provided by engineer. - - - Sending via - No comment provided by engineer. - - - Sent at - No comment provided by engineer. - - - Sent at: %@ - copied message info - - - Sent file event - notification - - - Sent message - message info title - - - Sent messages will be deleted after set time. - No comment provided by engineer. - - - Server requires authorization to create queues, check password - server test error - - - Server requires authorization to upload, check password - server test error - - - Server test failed! - No comment provided by engineer. - - - Servers - No comment provided by engineer. - - - Set 1 day - No comment provided by engineer. - - - Set contact name… - No comment provided by engineer. - - - Set group preferences - No comment provided by engineer. - - - Set it instead of system authentication. - No comment provided by engineer. - - - Set passcode - No comment provided by engineer. - - - Set passphrase to export - No comment provided by engineer. - - - Set the message shown to new members! - No comment provided by engineer. - - - Set timeouts for proxy/VPN - No comment provided by engineer. - - - Settings - No comment provided by engineer. - - - Share - chat item action - - - Share 1-time link - No comment provided by engineer. - - - Share address - No comment provided by engineer. - - - Share address with contacts? - No comment provided by engineer. - - - Share link - No comment provided by engineer. - - - Share one-time invitation link - No comment provided by engineer. - - - Share with contacts - No comment provided by engineer. - - - Show calls in phone history - No comment provided by engineer. - - - Show developer options - No comment provided by engineer. - - - Show preview - No comment provided by engineer. - - - Show: - No comment provided by engineer. - - - SimpleX Address - No comment provided by engineer. - - - SimpleX Chat security was audited by Trail of Bits. - No comment provided by engineer. - - - SimpleX Lock - No comment provided by engineer. - - - SimpleX Lock mode - No comment provided by engineer. - - - SimpleX Lock not enabled! - No comment provided by engineer. - - - SimpleX Lock turned on - No comment provided by engineer. - - - SimpleX address - No comment provided by engineer. - - - SimpleX contact address - simplex link type - - - SimpleX encrypted message or connection event - notification - - - SimpleX group link - simplex link type - - - SimpleX links - No comment provided by engineer. - - - SimpleX one-time invitation - simplex link type - - - Skip - No comment provided by engineer. - - - Skipped messages - 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. - - - Somebody - notification title - - - Start a new chat - No comment provided by engineer. - - - Start chat - No comment provided by engineer. - - - Start migration - No comment provided by engineer. - - - Stop - No comment provided by engineer. - - - Stop SimpleX - authentication reason - - - Stop chat to enable database actions - No comment provided by engineer. - - - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - No comment provided by engineer. - - - Stop chat? - No comment provided by engineer. - - - Stop file - cancel file action - - - Stop receiving file? - No comment provided by engineer. - - - Stop sending file? - No comment provided by engineer. - - - Stop sharing - No comment provided by engineer. - - - Stop sharing address? - No comment provided by engineer. - - - Submit - No comment provided by engineer. - - - Support SimpleX Chat - No comment provided by engineer. - - - System - No comment provided by engineer. - - - System authentication - No comment provided by engineer. - - - TCP connection timeout - No comment provided by engineer. - - - TCP_KEEPCNT - No comment provided by engineer. - - - TCP_KEEPIDLE - No comment provided by engineer. - - - TCP_KEEPINTVL - No comment provided by engineer. - - - Take picture - No comment provided by engineer. - - - Tap button - No comment provided by engineer. - - - Tap to activate profile. - No comment provided by engineer. - - - Tap to join - No comment provided by engineer. - - - Tap to join incognito - No comment provided by engineer. - - - Tap to start a new chat - No comment provided by engineer. - - - Test failed at step %@. - server test failure - - - Test server - No comment provided by engineer. - - - Test servers - No comment provided by engineer. - - - Tests failed! - No comment provided by engineer. - - - Thank you for installing SimpleX Chat! - 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)! - No comment provided by engineer. - - - Thanks to the users – contribute via Weblate! - No comment provided by engineer. - - - The 1st platform without any user identifiers – private by design. - No comment provided by engineer. - - - 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. - No comment provided by engineer. - - - The app can notify you when you receive messages or contact requests - please open settings to enable. - No comment provided by engineer. - - - The attempt to change database passphrase was not completed. - No comment provided by engineer. - - - The connection you accepted will be cancelled! - No comment provided by engineer. - - - The contact you shared this link with will NOT be able to connect! - No comment provided by engineer. - - - The created archive is available via app Settings / Database / Old database archive. - No comment provided by engineer. - - - The group is fully decentralized – it is visible only to the members. - No comment provided by engineer. - - - The hash of the previous message is different. - No comment provided by engineer. - - - The message will be deleted for all members. - No comment provided by engineer. - - - The message will be marked as moderated for all members. - No comment provided by engineer. - - - The next generation of private messaging - No comment provided by engineer. - - - The old database was not removed during the migration, it can be deleted. - No comment provided by engineer. - - - The profile is only shared with your contacts. - No comment provided by engineer. - - - The sender will NOT be notified - No comment provided by engineer. - - - The servers for new connections of your current chat profile **%@**. - No comment provided by engineer. - - - Theme - No comment provided by engineer. - - - There should be at least one user profile. - No comment provided by engineer. - - - There should be at least one visible user profile. - No comment provided by engineer. - - - This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - No comment provided by engineer. - - - This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - No comment provided by engineer. - - - This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - No comment provided by engineer. - - - This error is permanent for this connection, please re-connect. - No comment provided by engineer. - - - 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). - No comment provided by engineer. - - - This group no longer exists. - No comment provided by engineer. - - - This setting applies to messages in your current chat profile **%@**. - No comment provided by engineer. - - - To ask any questions and to receive updates: - No comment provided by engineer. - - - To connect, your contact can scan QR code or use the link in the app. - No comment provided by engineer. - - - To find the profile used for an incognito connection, tap the contact or group name on top of the chat. - No comment provided by engineer. - - - To make a new connection - 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. - No comment provided by engineer. - - - To protect timezone, image/voice files use UTC. - No comment provided by engineer. - - - To protect your information, turn on SimpleX Lock. -You will be prompted to complete authentication before this feature is enabled. - No comment provided by engineer. - - - To record voice message please grant permission to use Microphone. - No comment provided by engineer. - - - To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - No comment provided by engineer. - - - To support instant push notifications the chat database has to be migrated. - No comment provided by engineer. - - - To verify end-to-end encryption with your contact compare (or scan) the code on your devices. - No comment provided by engineer. - - - Transport isolation - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact (error: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - No comment provided by engineer. - - - Turn off - No comment provided by engineer. - - - Turn off notifications? - No comment provided by engineer. - - - Turn on - No comment provided by engineer. - - - Unable to record voice message - No comment provided by engineer. - - - Unexpected error: %@ - No comment provided by engineer. - - - Unexpected migration state - No comment provided by engineer. - - - Unhide - No comment provided by engineer. - - - Unhide chat profile - No comment provided by engineer. - - - Unhide profile - No comment provided by engineer. - - - Unit - No comment provided by engineer. - - - Unknown caller - callkit banner - - - Unknown database error: %@ - No comment provided by engineer. - - - Unknown error - No comment provided by engineer. - - - Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. - 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. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - No comment provided by engineer. - - - Unlock - No comment provided by engineer. - - - Unlock app - authentication reason - - - Unmute - No comment provided by engineer. - - - Unread - No comment provided by engineer. - - - Update - No comment provided by engineer. - - - Update .onion hosts setting? - 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? - 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 - No comment provided by engineer. - - - Upload file - server test step - - - Use .onion hosts - No comment provided by engineer. - - - Use SimpleX Chat servers? - No comment provided by engineer. - - - Use chat - No comment provided by engineer. - - - Use for new connections - No comment provided by engineer. - - - Use iOS call interface - No comment provided by engineer. - - - Use server - No comment provided by engineer. - - - User profile - No comment provided by engineer. - - - Using .onion hosts requires compatible VPN provider. - No comment provided by engineer. - - - Using SimpleX Chat servers. - No comment provided by engineer. - - - Verify connection security - No comment provided by engineer. - - - Verify security code - No comment provided by engineer. - - - Via browser - No comment provided by engineer. - - - Video call - No comment provided by engineer. - - - Video will be received when your contact completes uploading it. - No comment provided by engineer. - - - Video will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - Videos and files up to 1gb - No comment provided by engineer. - - - View security code - No comment provided by engineer. - - - Voice messages - chat feature - - - Voice messages are prohibited in this chat. - No comment provided by engineer. - - - Voice messages are prohibited in this group. - No comment provided by engineer. - - - Voice messages prohibited! - No comment provided by engineer. - - - Voice message… - No comment provided by engineer. - - - Waiting for file - No comment provided by engineer. - - - Waiting for image - No comment provided by engineer. - - - Waiting for video - No comment provided by engineer. - - - Warning: you may lose some data! - No comment provided by engineer. - - - WebRTC ICE servers - No comment provided by engineer. - - - Welcome %@! - No comment provided by engineer. - - - Welcome message - No comment provided by engineer. - - - What's new - No comment provided by engineer. - - - When available - No comment provided by engineer. - - - When people request to connect, you can accept or reject it. - No comment provided by engineer. - - - When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. - No comment provided by engineer. - - - With optional welcome message. - No comment provided by engineer. - - - Wrong database passphrase - No comment provided by engineer. - - - Wrong passphrase! - No comment provided by engineer. - - - XFTP servers - No comment provided by engineer. - - - You - No comment provided by engineer. - - - You accepted connection - No comment provided by engineer. - - - You allow - No comment provided by engineer. - - - You already have a chat profile with the same display name. Please choose another name. - No comment provided by engineer. - - - You are already connected to %@. - No comment provided by engineer. - - - You are connected to the server used to receive messages from this contact. - No comment provided by engineer. - - - You are invited to group - No comment provided by engineer. - - - You can accept calls from lock screen, without device and app authentication. - No comment provided by engineer. - - - You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button. - No comment provided by engineer. - - - You can create it later - No comment provided by engineer. - - - You can hide or mute a user profile - swipe it to the right. - No comment provided by engineer. - - - You can now send messages to %@ - notification body - - - You can set lock screen notification preview via settings. - No comment provided by engineer. - - - You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. - No comment provided by engineer. - - - You can share this address with your contacts to let them connect with **%@**. - No comment provided by engineer. - - - You can share your address as a link or QR code - anybody can connect to you. - No comment provided by engineer. - - - You can start chat via app Settings / Database or by restarting the app - No comment provided by engineer. - - - You can turn on SimpleX Lock via Settings. - No comment provided by engineer. - - - You can use markdown to format messages: - No comment provided by engineer. - - - You can't send messages! - No comment provided by engineer. - - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - - - You could not be verified; please try again. - No comment provided by engineer. - - - You have no chats - No comment provided by engineer. - - - You have to enter passphrase every time the app starts - it is not stored on the device. - No comment provided by engineer. - - - You invited your contact - No comment provided by engineer. - - - You joined this group - No comment provided by engineer. - - - You joined this group. Connecting to inviting group member. - No comment provided by engineer. - - - You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. - No comment provided by engineer. - - - You need to allow your contact to send voice messages to be able to send them. - No comment provided by engineer. - - - You rejected group invitation - No comment provided by engineer. - - - You sent group invitation - No comment provided by engineer. - - - You will be connected to group when the group host's device is online, please wait or check later! - No comment provided by engineer. - - - You will be connected when your connection request is accepted, please wait or check later! - No comment provided by engineer. - - - You will be connected when your contact's device is online, please wait or check later! - No comment provided by engineer. - - - You will be required to authenticate when you start or resume the app after 30 seconds in background. - No comment provided by engineer. - - - You will join a group this link refers to and connect to its group members. - No comment provided by engineer. - - - You will still receive calls and notifications from muted profiles when they are active. - No comment provided by engineer. - - - You will stop receiving messages from this group. Chat history will be preserved. - No comment provided by engineer. - - - You won't lose your contacts if you later delete your address. - No comment provided by engineer. - - - You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile - No comment provided by engineer. - - - You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed - No comment provided by engineer. - - - Your %@ servers - No comment provided by engineer. - - - Your ICE servers - No comment provided by engineer. - - - Your SMP servers - No comment provided by engineer. - - - Your SimpleX address - No comment provided by engineer. - - - Your XFTP servers - No comment provided by engineer. - - - Your calls - No comment provided by engineer. - - - Your chat database - No comment provided by engineer. - - - Your chat database is not encrypted - set passphrase to encrypt it. - No comment provided by engineer. - - - Your chat profile will be sent to group members - No comment provided by engineer. - - - Your chat profile will be sent to your contact - No comment provided by engineer. - - - Your chat profiles - No comment provided by engineer. - - - Your chats - No comment provided by engineer. - - - 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). - 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. -You can change it in Settings. - No comment provided by engineer. - - - Your contacts will remain connected. - No comment provided by engineer. - - - Your current chat database will be DELETED and REPLACED with the imported one. - No comment provided by engineer. - - - Your current profile - No comment provided by engineer. - - - Your preferences - No comment provided by engineer. - - - Your privacy - No comment provided by engineer. - - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - No comment provided by engineer. - - - Your profile will be sent to the contact that you received this link from - No comment provided by engineer. - - - Your profile, contacts and delivered messages are stored on your device. - No comment provided by engineer. - - - Your random profile - No comment provided by engineer. - - - Your server - No comment provided by engineer. - - - Your server address - No comment provided by engineer. - - - Your settings - No comment provided by engineer. - - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - - - [Send us email](mailto:chat@simplex.chat) - No comment provided by engineer. - - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - - - \_italic_ - No comment provided by engineer. - - - \`a + b` - No comment provided by engineer. - - - above, then choose: - No comment provided by engineer. - - - accepted call - call status - - - admin - member role - - - always - pref value - - - audio call (not e2e encrypted) - No comment provided by engineer. - - - bad message ID - integrity error chat item - - - bad message hash - integrity error chat item - - - bold - No comment provided by engineer. - - - call error - call status - - - call in progress - call status - - - calling… - call status - - - cancelled %@ - feature offered item - - - changed address for you - chat item text - - - changed role of %1$@ to %2$@ - rcv group event chat item - - - changed your role to %@ - rcv group event chat item - - - changing address for %@... - chat item text - - - changing address... - chat item text - - - colored - No comment provided by engineer. - - - complete - No comment provided by engineer. - - - connect to SimpleX Chat developers. - No comment provided by engineer. - - - connected - No comment provided by engineer. - - - connecting - No comment provided by engineer. - - - connecting (accepted) - No comment provided by engineer. - - - connecting (announced) - No comment provided by engineer. - - - connecting (introduced) - No comment provided by engineer. - - - connecting (introduction invitation) - No comment provided by engineer. - - - connecting call… - call status - - - connecting… - chat list item title - - - connection established - chat list item title (it should not be shown - - - connection:%@ - connection information - - - contact has e2e encryption - No comment provided by engineer. - - - contact has no e2e encryption - No comment provided by engineer. - - - creator - No comment provided by engineer. - - - custom - dropdown time picker choice - - - database version is newer than the app, but no down migration for: %@ - No comment provided by engineer. - - - days - time unit - - - default (%@) - pref value - - - deleted - deleted chat item - - - deleted group - rcv group event chat item - - - different migration in the app/database: %@ / %@ - No comment provided by engineer. - - - direct - connection level description - - - duplicate message - integrity error chat item - - - e2e encrypted - No comment provided by engineer. - - - enabled - enabled status - - - enabled for contact - enabled status - - - enabled for you - enabled status - - - ended - No comment provided by engineer. - - - ended call %@ - call status - - - error - No comment provided by engineer. - - - group deleted - No comment provided by engineer. - - - group profile updated - snd group event chat item - - - hours - time unit - - - iOS Keychain is used to securely store passphrase - it allows receiving push notifications. - 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. - No comment provided by engineer. - - - incognito via contact address link - chat list item description - - - incognito via group link - chat list item description - - - incognito via one-time link - chat list item description - - - indirect (%d) - connection level description - - - invalid chat - invalid chat data - - - invalid chat data - No comment provided by engineer. - - - invalid data - invalid chat item - - - invitation to group %@ - group name - - - invited - No comment provided by engineer. - - - invited %@ - rcv group event chat item - - - invited to connect - chat list item title - - - invited via your group link - rcv group event chat item - - - italic - No comment provided by engineer. - - - join as %@ - No comment provided by engineer. - - - left - rcv group event chat item - - - marked deleted - marked deleted chat item preview text - - - member - member role - - - connected - rcv group event chat item - - - message received - notification - - - minutes - time unit - - - missed call - call status - - - moderated - moderated chat item - - - moderated by %@ - No comment provided by engineer. - - - months - time unit - - - never - No comment provided by engineer. - - - new message - notification - - - no - pref value - - - no e2e encryption - No comment provided by engineer. - - - no text - copied message info in history - - - observer - member role - - - off - enabled status - group pref value - - - offered %@ - feature offered item - - - offered %1$@: %2$@ - feature offered item - - - on - group pref value - - - or chat with the developers - No comment provided by engineer. - - - owner - member role - - - peer-to-peer - No comment provided by engineer. - - - received answer… - No comment provided by engineer. - - - received confirmation… - No comment provided by engineer. - - - rejected call - call status - - - removed - No comment provided by engineer. - - - removed %@ - rcv group event chat item - - - removed you - rcv group event chat item - - - sec - network option - - - seconds - time unit - - - secret - No comment provided by engineer. - - - starting… - No comment provided by engineer. - - - strike - No comment provided by engineer. - - - this contact - notification title - - - unknown - connection info - - - updated group profile - rcv group event chat item - - - v%@ (%@) - No comment provided by engineer. - - - via contact address link - chat list item description - - - via group link - chat list item description - - - via one-time link - chat list item description - - - via relay - No comment provided by engineer. - - - video call (not e2e encrypted) - No comment provided by engineer. - - - waiting for answer… - No comment provided by engineer. - - - waiting for confirmation… - No comment provided by engineer. - - - wants to connect to you! - No comment provided by engineer. - - - weeks - time unit - - - yes - pref value - - - you are invited to group - No comment provided by engineer. - - - you are observer - No comment provided by engineer. - - - you changed address - chat item text - - - you changed address for %@ - chat item text - - - you changed role for yourself to %@ - snd group event chat item - - - you changed role of %1$@ to %2$@ - snd group event chat item - - - you left - snd group event chat item - - - you removed %@ - snd group event chat item - - - you shared one-time link - chat list item description - - - you shared one-time link incognito - chat list item description - - - you: - No comment provided by engineer. - - - \~strike~ - No comment provided by engineer. - - -
- -
- -
- - - SimpleX - Bundle name - - - SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - Privacy - Camera Usage Description - - - SimpleX uses Face ID for local authentication - Privacy - Face ID Usage Description - - - SimpleX needs microphone access for audio and video calls, and to record voice messages. - Privacy - Microphone Usage Description - - - SimpleX needs access to Photo Library for saving captured and received media - Privacy - Photo Library Additions Usage Description - - -
- -
- -
- - - SimpleX NSE - Bundle display name - - - SimpleX NSE - Bundle name - - - Copyright © 2022 SimpleX Chat. All rights reserved. - Copyright (human-readable) - - -
-
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 c5a9ba4ea8..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 @@
- +
- - - - - - 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) @@ -127,6 +100,11 @@ %@ is geverifieerd No comment provided by engineer. + + %@ server + %@ server + No comment provided by engineer. + %@ servers %@ servers @@ -142,6 +120,11 @@ %@ wil verbinding maken! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ en %lld leden @@ -162,11 +145,36 @@ %d dagen time interval + + %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. + forward confirmation reason + %d hours %d uren time interval + + %d messages not forwarded + %d berichten niet doorgestuurd + alert title + %d min %d min @@ -182,6 +190,11 @@ %d sec time interval + + %d seconds(s) + %d seconden + delete after time + %d skipped message(s) %d overgeslagen bericht(en) @@ -252,11 +265,6 @@ %lld nieuwe interface-talen No comment provided by engineer. - - %lld second(s) - %lld seconde(n) - No comment provided by engineer. - %lld seconds %lld seconden @@ -307,11 +315,6 @@ %u berichten zijn overgeslagen. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nieuw) @@ -322,33 +325,23 @@ (dit apparaat v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **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. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Nieuw contact toevoegen**: om uw eenmalige QR-code of link voor uw contact te maken. - No comment provided by engineer. - **Create group**: to create a new group. **Groep aanmaken**: om een nieuwe groep aan te maken. 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. + + **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). + + **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. @@ -362,11 +355,16 @@ **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. + + **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 / 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. @@ -392,11 +390,6 @@ \*vetgedrukt* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +426,6 @@ - bewerkingsgeschiedenis. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sec @@ -451,7 +439,8 @@ 1 day 1 dag - time interval + delete after time +time interval 1 hour @@ -466,12 +455,29 @@ 1 month 1 maand - time interval + delete after time +time interval 1 week 1 week - time interval + delete after time +time interval + + + 1 year + 1 jaar + delete after time + + + 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 @@ -488,11 +494,6 @@ 30 seconden No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -517,7 +518,7 @@ 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**. + Er wordt een aparte TCP-verbinding gebruikt **voor elk chatprofiel dat je in de app hebt**. No comment provided by engineer. @@ -542,31 +543,32 @@ Adres wijziging afbreken? No comment provided by engineer. - - About SimpleX - Over SimpleX - No comment provided by engineer. - About SimpleX Chat Over SimpleX Chat No comment provided by engineer. - - About SimpleX address - Over SimpleX adres + + About operators + Over operatoren No comment provided by engineer. - - Accent color - Accent kleur + + Accent + Accent No comment provided by engineer. Accept Accepteer accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Accepteer voorwaarden + No comment provided by engineer. Accept connection request? @@ -581,21 +583,47 @@ Accept incognito Accepteer incognito - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Geaccepteerde voorwaarden + No comment provided by engineer. + + + Acknowledged + Erkend + No comment provided by engineer. + + + Acknowledgement errors + Bevestigingsfouten + No comment provided by engineer. + + + Active + actief + token status text + + + 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 contact - Contact toevoegen + + Add friends + Vrienden toevoegen No comment provided by engineer. - - Add preset servers - Vooraf ingestelde servers toevoegen + + Add list + Lijst toevoegen No comment provided by engineer. @@ -603,14 +631,19 @@ Profiel toevoegen No comment provided by engineer. + + 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 server… - Server toevoegen… + + Add team members + Teamleden toevoegen No comment provided by engineer. @@ -618,9 +651,44 @@ Toevoegen aan een ander apparaat No comment provided by engineer. + + Add to list + Toevoegen aan lijst + No comment provided by engineer. + Add welcome message - Welkomst bericht toevoegen + 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. @@ -633,6 +701,16 @@ 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 + No comment provided by engineer. + Admins can block a member for all. Beheerders kunnen een lid voor iedereen blokkeren. @@ -648,6 +726,16 @@ Geavanceerde netwerk instellingen No comment provided by engineer. + + Advanced settings + Geavanceerde instellingen + No comment provided by engineer. + + + All + alle + No comment provided by engineer. + All app data is deleted. Alle app-gegevens worden verwijderd. @@ -658,16 +746,31 @@ Alle chats en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + Alle chats worden uit de lijst %@ verwijderd en de lijst wordt verwijderd. + alert message + 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. 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! @@ -683,6 +786,20 @@ Alle nieuwe berichten van %@ worden verborgen! No comment provided by engineer. + + All profiles + Alle profielen + profile dropdown + + + All reports will be archived for you. + Alle rapporten worden voor u gearchiveerd. + No comment provided by engineer. + + + All servers + No comment provided by engineer. + All your contacts will remain connected. Al uw contacten blijven verbonden. @@ -708,24 +825,34 @@ 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 downgrade + Downgraden toestaan + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) - Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) + Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) No comment provided by engineer. Allow message reactions only if your contact allows them. - Sta berichtreacties alleen toe als uw contact dit toestaat. + Sta bericht reacties alleen toe als uw contact dit toestaat. No comment provided by engineer. Allow message reactions. - Sta berichtreacties toe. + Sta bericht reacties toe. No comment provided by engineer. @@ -738,9 +865,19 @@ Toestaan dat verdwijnende berichten worden verzonden. No comment provided by engineer. + + Allow sharing + Delen toestaan + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) - Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur) + 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. @@ -770,7 +907,7 @@ Allow your contacts adding message reactions. - Sta uw contactpersonen toe om berichtreacties toe te voegen. + Sta uw contactpersonen toe om bericht reacties toe te voegen. No comment provided by engineer. @@ -780,7 +917,7 @@ Allow your contacts to irreversibly delete sent messages. (24 hours) - Laat uw contacten verzonden berichten onomkeerbaar verwijderen. (24 uur) + Laat uw contacten verzonden berichten definitief verwijderen. (24 uur) No comment provided by engineer. @@ -808,6 +945,11 @@ Al lid van de groep! No comment provided by engineer. + + Always use private routing. + Gebruik altijd privéroutering. + No comment provided by engineer. + Always use relay Altijd relay gebruiken @@ -818,11 +960,21 @@ Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend. No comment provided by engineer. + + Another reason + Een andere reden + report reason + 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: %@ @@ -838,6 +990,11 @@ 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 @@ -853,6 +1010,11 @@ 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 @@ -873,11 +1035,56 @@ 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? + No comment provided by engineer. + + + Archive reports + Rapporten archiveren + swipe action + + + Archived contacts + Gearchiveerde contacten + No comment provided by engineer. + Archiving database Database archiveren @@ -905,7 +1112,7 @@ Audio/video calls are prohibited. - Audio/video gesprekken zijn verboden. + Audio/video gesprekken zijn niet toegestaan. No comment provided by engineer. @@ -943,11 +1150,21 @@ Afbeeldingen automatisch accepteren No comment provided by engineer. + + Auto-accept settings + Instellingen automatisch accepteren + alert title + Back Terug No comment provided by engineer. + + Background + Achtergrond + No comment provided by engineer. + Bad desktop address Onjuist desktopadres @@ -963,16 +1180,61 @@ Onjuiste bericht hash 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 @@ -1008,9 +1270,19 @@ 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. + Both you and your contact can add message reactions. - Zowel u als uw contact kunnen berichtreacties toevoegen. + Zowel u als uw contact kunnen bericht reacties toevoegen. No comment provided by engineer. @@ -1038,9 +1310,33 @@ 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. + + 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 chat profiel (standaard) of [via verbinding](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: +- send only legal content in public groups. +- respect other users – no spam. + Door SimpleX Chat te gebruiken, gaat u ermee akkoord: +- alleen legale content te versturen in openbare groepen. +- andere gebruikers te respecteren – geen spam. No comment provided by engineer. @@ -1053,11 +1349,26 @@ Oproepen No comment provided by engineer. + + 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! @@ -1068,10 +1379,16 @@ Kan geen contacten uitnodigen! No comment provided by engineer. + + Can't message member + Kan geen bericht sturen naar lid + No comment provided by engineer. + Cancel Annuleren - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1400,20 @@ Geen toegang tot de keychain om database wachtwoord op te slaan No comment provided by engineer. + + Cannot forward message + Kan bericht niet doorsturen + No comment provided by engineer. + Cannot receive file Kan bestand niet ontvangen - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Capaciteit overschreden - ontvanger heeft eerder verzonden berichten niet ontvangen. + snd error text Cellular @@ -1098,6 +1425,16 @@ Veranderen No comment provided by engineer. + + Change automatic message deletion? + Automatisch verwijderen van berichten wijzigen? + alert title + + + Change chat profiles + Gebruikersprofielen wijzigen + authentication reason + Change database passphrase? Wachtwoord database wijzigen? @@ -1142,11 +1479,26 @@ Change self-destruct passcode Zelfvernietigings code wijzigen authentication reason - set passcode view +set passcode view - - 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. @@ -1164,6 +1516,11 @@ 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 @@ -1184,6 +1541,11 @@ 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! @@ -1194,15 +1556,50 @@ Gesprek voorkeuren No comment provided by engineer. + + Chat preferences were changed. + Chatvoorkeuren zijn gewijzigd. + alert message + + + 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 - Gesprekken + 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. No comment provided by engineer. Check server address and try again. Controleer het server adres en probeer het opnieuw. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1621,25 @@ 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 + No comment provided by engineer. + Clear Wissen - No comment provided by engineer. + swipe action Clear conversation @@ -1239,6 +1651,16 @@ 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? @@ -1249,11 +1671,21 @@ Verwijderd verificatie No comment provided by engineer. - - Colors - Kleuren + + 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. + + + Community guidelines violation + Schending van de communityrichtlijnen + report reason + Compare file Bestand vergelijken @@ -1264,11 +1696,56 @@ Vergelijk beveiligingscodes met je contacten. 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 @@ -1279,11 +1756,21 @@ Bevestig toegangscode 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 @@ -1309,6 +1796,11 @@ Bevestig het uploaden No comment provided by engineer. + + Confirmed + Bevestigd + token status text + Connect Verbind @@ -1329,6 +1821,11 @@ Verbinden met desktop 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? @@ -1368,16 +1865,31 @@ Dit is uw eigen eenmalige link! Verbonden met %@ No comment provided by engineer. + + Connected + Verbonden + No comment provided by engineer. + Connected desktop Verbonden desktop 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 to server… Verbinden met de server… @@ -1388,6 +1900,11 @@ Dit is uw eigen eenmalige link! 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 @@ -1398,6 +1915,16 @@ Dit is uw eigen eenmalige link! 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 @@ -1408,11 +1935,38 @@ Dit is uw eigen eenmalige link! Verbindingsfout (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + Verbinding is geblokkeerd door serveroperator: +%@ + 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 @@ -1423,6 +1977,16 @@ Dit is uw eigen eenmalige link! Timeout verbinding No comment provided by engineer. + + Connection with desktop stopped + Verbinding met desktop is gestopt + No comment provided by engineer. + + + Connections + Verbindingen + No comment provided by engineer. + Contact allows Contact maakt het mogelijk @@ -1433,6 +1997,11 @@ Dit is uw eigen eenmalige link! Contact bestaat al No comment provided by engineer. + + Contact deleted! + Contact verwijderd! + No comment provided by engineer. + Contact hidden: Contact verborgen: @@ -1443,9 +2012,9 @@ Dit is uw eigen eenmalige link! Contact is verbonden notification - - Contact is not connected yet! - Contact is nog niet verbonden! + + Contact is deleted. + Contact is verwijderd. No comment provided by engineer. @@ -1458,6 +2027,11 @@ Dit is uw eigen eenmalige link! 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 @@ -1468,21 +2042,41 @@ Dit is uw eigen eenmalige link! Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken. No comment provided by engineer. + + Content violates conditions of use + Inhoud schendt de gebruiksvoorwaarden + blocking reason + Continue Doorgaan No comment provided by engineer. + + Conversation deleted! + Gesprek verwijderd! + No comment provided by engineer. + Copy Kopiëren - chat item action + 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 %@? @@ -1493,6 +2087,11 @@ Dit is uw eigen eenmalige link! Maak No comment provided by engineer. + + Create 1-time link + Eenmalige link maken + No comment provided by engineer. + Create SimpleX address Maak een SimpleX adres aan @@ -1503,11 +2102,6 @@ Dit is uw eigen eenmalige link! Maak een groep met een willekeurig profiel. 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. - No comment provided by engineer. - Create file Bestand maken @@ -1528,6 +2122,11 @@ Dit is uw eigen eenmalige link! Maak link No comment provided by engineer. + + Create list + Maak een lijst + 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/). 💻 @@ -1553,6 +2152,11 @@ Dit is uw eigen eenmalige link! Maak je profiel aan No comment provided by engineer. + + Created + Gemaakt + No comment provided by engineer. + Created at Gemaakt op @@ -1563,11 +2167,6 @@ Dit is uw eigen eenmalige link! Aangemaakt op: %@ copied message info - - Created on %@ - Gemaakt op %@ - No comment provided by engineer. - Creating archive link Archief link maken @@ -1583,11 +2182,21 @@ Dit is uw eigen eenmalige link! Huidige toegangscode 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 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 %@. @@ -1598,11 +2207,26 @@ Dit is uw eigen eenmalige link! 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 ID Database-ID @@ -1674,7 +2298,7 @@ Dit is uw eigen eenmalige link! Database passphrase is required to open chat. - Database wachtwoord is vereist om je gesprekken te openen. + Database wachtwoord is vereist om je chats te openen. No comment provided by engineer. @@ -1701,6 +2325,11 @@ Dit is uw eigen eenmalige link! De database wordt gemigreerd wanneer de app opnieuw wordt opgestart No comment provided by engineer. + + Debug delivery + Foutopsporing bezorging + No comment provided by engineer. + Decentralized Gedecentraliseerd @@ -1714,18 +2343,19 @@ Dit is uw eigen eenmalige link! Delete Verwijderen - chat item action + alert action +swipe action + + + Delete %lld messages of members? + %lld berichten van leden verwijderen? + No comment provided by engineer. Delete %lld messages? %lld berichten verwijderen? No comment provided by engineer. - - Delete Contact - Verwijder contact - No comment provided by engineer. - Delete address Adres verwijderen @@ -1751,24 +2381,29 @@ Dit is uw eigen eenmalige link! 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 + Chatprofiel verwijderen No comment provided by engineer. Delete chat profile? - Chat profiel verwijderen? + Chatprofiel verwijderen? + No comment provided by engineer. + + + Delete chat? + Chat verwijderen? No comment provided by engineer. @@ -1781,11 +2416,9 @@ Dit is uw eigen eenmalige link! Verwijder contact No comment provided by engineer. - - Delete contact? -This cannot be undone! - Verwijder contact? -Dit kan niet ongedaan gemaakt worden! + + Delete contact? + Verwijder contact? No comment provided by engineer. @@ -1810,7 +2443,7 @@ Dit kan niet ongedaan gemaakt worden! Delete files for all chat profiles - Verwijder bestanden voor alle chat profielen + Verwijder bestanden voor alle chatprofielen No comment provided by engineer. @@ -1848,6 +2481,11 @@ Dit kan niet ongedaan gemaakt worden! Link verwijderen? No comment provided by engineer. + + Delete list? + Lijst verwijderen? + alert title + Delete member message? Bericht van lid verwijderen? @@ -1861,7 +2499,7 @@ Dit kan niet ongedaan gemaakt worden! Delete messages Verwijder berichten - No comment provided by engineer. + alert button Delete messages after @@ -1878,9 +2516,9 @@ Dit kan niet ongedaan gemaakt worden! 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. @@ -1898,11 +2536,31 @@ Dit kan niet ongedaan gemaakt worden! Wachtrij verwijderen server test step + + 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 + No comment provided by engineer. + + + Deleted + Verwijderd + No comment provided by engineer. + Deleted at Verwijderd om @@ -1913,6 +2571,16 @@ Dit kan niet ongedaan gemaakt worden! Verwijderd om: %@ copied message info + + 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 @@ -1948,11 +2616,41 @@ Dit kan niet ongedaan gemaakt worden! 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 %@. + No comment provided by engineer. + + + Destination server error: %@ + Bestemmingsserverfout: %@ + snd error text + + + 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 @@ -1983,9 +2681,14 @@ Dit kan niet ongedaan gemaakt worden! Directe berichten chat feature - - 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. @@ -1998,11 +2701,26 @@ Dit kan niet ongedaan gemaakt worden! SimpleX Vergrendelen uitschakelen authentication reason + + Disable automatic message deletion? + Automatisch verwijderen van berichten uitschakelen? + alert title + + + Disable delete messages + Berichten verwijderen uitschakelen + alert button + Disable for all Uitschakelen voor iedereen No comment provided by engineer. + + Disabled + Uitgeschakeld + No comment provided by engineer. + Disappearing message Verdwijnend bericht @@ -2015,12 +2733,12 @@ Dit kan niet ongedaan gemaakt worden! Disappearing messages are prohibited in this chat. - Verdwijnende berichten zijn verboden in dit gesprek. + 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. @@ -2053,11 +2771,21 @@ Dit kan niet ongedaan gemaakt worden! Ontdek via het lokale netwerk 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 SimpleX for emergency calls. Gebruik SimpleX NIET voor noodoproepen. No comment provided by engineer. + + Do NOT use private routing. + Gebruik GEEN privéroutering. + No comment provided by engineer. + Do it later Doe het later @@ -2068,6 +2796,16 @@ Dit kan niet ongedaan gemaakt worden! Stuur geen geschiedenis naar nieuwe leden. No comment provided by engineer. + + Do not use credentials with proxy. + Gebruik geen inloggegevens met proxy. + No comment provided by engineer. + + + Documents: + Documenten: + No comment provided by engineer. + Don't create address Maak geen adres aan @@ -2078,11 +2816,21 @@ Dit kan niet ongedaan gemaakt worden! 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 @@ -2091,7 +2839,13 @@ Dit kan niet ongedaan gemaakt worden! Download Downloaden - chat item action + alert button +chat item action + + + Download errors + Downloadfouten + No comment provided by engineer. Download failed @@ -2103,6 +2857,21 @@ Dit kan niet ongedaan gemaakt worden! Download bestand server test step + + Download files + ‐Bestanden downloaden + alert action + + + Downloaded + Gedownload + No comment provided by engineer. + + + Downloaded files + Gedownloade bestanden + No comment provided by engineer. + Downloading archive Archief downloaden @@ -2123,6 +2892,11 @@ Dit kan niet ongedaan gemaakt worden! Duur No comment provided by engineer. + + E2E encrypted notifications. + E2E versleutelde meldingen. + No comment provided by engineer. + Edit Bewerk @@ -2143,6 +2917,11 @@ Dit kan niet ongedaan gemaakt worden! Inschakelen (overschrijvingen behouden) 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 SimpleX Lock SimpleX Vergrendelen inschakelen @@ -2156,7 +2935,7 @@ Dit kan niet ongedaan gemaakt worden! Enable automatic message deletion? Automatisch verwijderen van berichten aanzetten? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2982,11 @@ Dit kan niet ongedaan gemaakt worden! Zelfvernietigings wachtwoord inschakelen set passcode view + + Enabled + Ingeschakeld + No comment provided by engineer. + Enabled for Ingeschakeld voor @@ -2278,6 +3062,11 @@ Dit kan niet ongedaan gemaakt worden! Opnieuw onderhandelen over de codering is mislukt. No comment provided by engineer. + + Encryption renegotiation in progress. + Er wordt opnieuw onderhandeld over de encryptie. + No comment provided by engineer. + Enter Passcode Voer toegangscode in @@ -2320,12 +3109,12 @@ Dit kan niet ongedaan gemaakt worden! Enter welcome message… - Welkomst bericht invoeren… + Welkom bericht invoeren… placeholder Enter welcome message… (optional) - Voer welkomst bericht in... (optioneel) + Voer welkom bericht in... (optioneel) placeholder @@ -2343,26 +3132,36 @@ Dit kan niet ongedaan gemaakt worden! Fout bij het afbreken van adres wijziging No comment provided by engineer. + + Error accepting conditions + Fout bij het accepteren van voorwaarden + alert title + 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 No comment provided by engineer. + + Error adding server + Fout bij toevoegen server + alert title + 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 @@ -2373,6 +3172,21 @@ Dit kan niet ongedaan gemaakt worden! 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 @@ -2388,6 +3202,11 @@ Dit kan niet ongedaan gemaakt worden! Fout bij maken van groep link No comment provided by engineer. + + Error creating list + Fout bij het aanmaken van de lijst + alert title + Error creating member contact Fout bij aanmaken contact @@ -2403,6 +3222,11 @@ Dit kan niet ongedaan gemaakt worden! 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 @@ -2423,11 +3247,6 @@ Dit kan niet ongedaan gemaakt worden! 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 @@ -2473,6 +3292,11 @@ Dit kan niet ongedaan gemaakt worden! 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 @@ -2483,9 +3307,14 @@ Dit kan niet ongedaan gemaakt worden! Fout bij lid worden van groep No comment provided by engineer. - - Error loading %@ servers - Fout bij het laden van %@ servers + + Error loading servers + Fout bij het laden van servers + alert title + + + Error migrating settings + Fout bij migreren van instellingen No comment provided by engineer. @@ -2496,16 +3325,36 @@ Dit kan niet ongedaan gemaakt worden! Error receiving file Fout bij ontvangen van bestand + alert title + + + 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 + No comment provided by engineer. + + + Error registering for notifications + Fout bij registreren voor meldingen + alert title + Error removing member Fout bij verwijderen van lid No comment provided by engineer. - - Error saving %@ servers - Fout bij opslaan van %@ servers + + Error reordering lists + Fout bij het opnieuw ordenen van lijsten + alert title + + + Error resetting statistics + Fout bij het resetten van statistieken No comment provided by engineer. @@ -2513,6 +3362,11 @@ Dit kan niet ongedaan gemaakt worden! Fout bij opslaan van ICE servers No comment provided by engineer. + + Error saving chat list + Fout bij het opslaan van chatlijst + alert title + Error saving group profile Fout bij opslaan van groep profiel @@ -2528,6 +3382,11 @@ Dit kan niet ongedaan gemaakt worden! Fout bij opslaan van wachtwoord in de keychain No comment provided by engineer. + + Error saving servers + Fout bij het opslaan van servers + alert title + Error saving settings Fout bij opslaan van instellingen @@ -2573,16 +3432,26 @@ Dit kan niet ongedaan gemaakt worden! Fout bij het stoppen van de chat No comment provided by engineer. + + Error switching profile + Fout bij wisselen van profiel + No comment provided by engineer. + Error switching profile! Fout bij wisselen van profiel! - No comment provided by engineer. + alertTitle 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 @@ -2593,6 +3462,11 @@ Dit kan niet ongedaan gemaakt worden! Fout bij updaten van bericht No comment provided by engineer. + + Error updating server + Fout bij het updaten van de server + alert title + Error updating settings Fout bij bijwerken van instellingen @@ -2621,7 +3495,9 @@ Dit kan niet ongedaan gemaakt worden! Error: %@ Fout: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3509,16 @@ Dit kan niet ongedaan gemaakt worden! Fout: geen database bestand No comment provided by engineer. + + Errors + Fouten + No comment provided by engineer. + + + Errors in servers configuration. + Fouten in de serverconfiguratie. + servers error + Even when disabled in the conversation. Zelfs wanneer uitgeschakeld in het gesprek. @@ -2648,6 +3534,11 @@ Dit kan niet ongedaan gemaakt worden! Uitklappen chat item action + + Expired + Verlopen + token status text + Export database Database exporteren @@ -2658,6 +3549,11 @@ Dit kan niet ongedaan gemaakt worden! Exportfout: No comment provided by engineer. + + Export theme + Exporteer thema + No comment provided by engineer. + Exported database archive. Geëxporteerd database archief. @@ -2683,16 +3579,70 @@ Dit kan niet ongedaan gemaakt worden! 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. + No comment provided by engineer. + Favorite Favoriet + swipe action + + + Favorites + Favorieten No comment provided by engineer. + + File error + Bestandsfout + file error alert title + + + File errors: +%@ + Bestandsfouten: +%@ + alert message + + + File is blocked by server operator: +%@. + Bestand is geblokkeerd door serveroperator: +%@. + 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: %@ + file error text + + + File status + Bestandsstatus + No comment provided by engineer. + + + File status: %@ + Bestandsstatus: %@ + copied message info + File will be deleted from servers. Het bestand wordt van de servers verwijderd. @@ -2713,6 +3663,11 @@ Dit kan niet ongedaan gemaakt worden! Bestand: %@ No comment provided by engineer. + + Files + Bestanden + No comment provided by engineer. + Files & media Bestanden en media @@ -2723,9 +3678,9 @@ Dit kan niet ongedaan gemaakt worden! Bestanden en media chat feature - - 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. @@ -2735,7 +3690,7 @@ Dit kan niet ongedaan gemaakt worden! Files and media prohibited! - Bestanden en media verboden! + Bestanden en media niet toegestaan! No comment provided by engineer. @@ -2760,7 +3715,7 @@ Dit kan niet ongedaan gemaakt worden! Find chats faster - Vind gesprekken sneller + Vind chats sneller No comment provided by engineer. @@ -2793,21 +3748,71 @@ Dit kan niet ongedaan gemaakt worden! Herstel wordt niet ondersteund door groepslid No comment provided by engineer. + + For all moderators + Voor alle moderators + No comment provided by engineer. + + + For chat profile %@: + Voor chatprofiel %@: + servers error + 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 + No comment provided by engineer. + Forward Doorsturen chat item action + + Forward %d message(s)? + %d bericht(en) doorsturen? + alert title + Forward and save messages Berichten doorsturen en opslaan No comment provided by engineer. + + Forward messages + Berichten doorsturen + alert action + + + Forward messages without files? + Berichten doorsturen zonder bestanden? + alert message + + + Forward up to 20 messages at once. + Stuur maximaal 20 berichten tegelijk door. + No comment provided by engineer. + Forwarded Doorgestuurd @@ -2818,6 +3823,40 @@ Dit kan niet ongedaan gemaakt worden! 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Doorstuurserver: %1$@ +Bestemmingsserverfout: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Doorstuurserver: %1$@ +Fout: %2$@ + snd error text + Found desktop Desktop gevonden @@ -2838,11 +3877,6 @@ Dit kan niet ongedaan gemaakt worden! 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. @@ -2863,6 +3897,21 @@ Dit kan niet ongedaan gemaakt worden! GIF's en stickers No comment provided by engineer. + + Get notified when mentioned. + Ontvang een melding als u vermeld wordt. + No comment provided by engineer. + + + Good afternoon! + Goedemiddag! + message preview + + + Good morning! + Goedemorgen! + message preview + Group Groep @@ -2918,41 +3967,6 @@ Dit kan niet ongedaan gemaakt worden! 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. (24 hours) - Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur) - No comment provided by engineer. - - - Group members can send SimpleX links. - Groepsleden kunnen SimpleX-links verzenden. - 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. - No comment provided by engineer. - Group message: Groep bericht: @@ -2980,7 +3994,7 @@ Dit kan niet ongedaan gemaakt worden! Group welcome message - Groep welkomst bericht + Groep welkom bericht No comment provided by engineer. @@ -2993,11 +4007,21 @@ Dit kan niet ongedaan gemaakt worden! 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 @@ -3005,7 +4029,7 @@ Dit kan niet ongedaan gemaakt worden! Hidden chat profiles - Verborgen chat profielen + Verborgen chatprofielen No comment provided by engineer. @@ -3048,10 +4072,20 @@ Dit kan niet ongedaan gemaakt worden! Hoe SimpleX werkt 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 + No comment provided by engineer. + How it works Hoe het werkt - No comment provided by engineer. + alert button How to @@ -3078,6 +4112,11 @@ Dit kan niet ongedaan gemaakt worden! ICE servers (één per lijn) No comment provided by engineer. + + IP address + IP-adres + No comment provided by engineer. + 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. @@ -3085,7 +4124,7 @@ Dit kan niet ongedaan gemaakt worden! 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! + Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens definitief verwijderd! No comment provided by engineer. @@ -3118,8 +4157,8 @@ Dit kan niet ongedaan gemaakt worden! Onmiddellijk No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Immuun voor spam en misbruik No comment provided by engineer. @@ -3143,11 +4182,23 @@ Dit kan niet ongedaan gemaakt worden! 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. +More improvements are coming soon! + Verbeterde levering, minder data gebruik. +Binnenkort meer verbeteringen! + No comment provided by engineer. + Improved message delivery Verbeterde berichtbezorging @@ -3178,6 +4229,16 @@ Dit kan niet ongedaan gemaakt worden! Geluiden tijdens het bellen No comment provided by engineer. + + Inappropriate content + Ongepaste inhoud + report reason + + + Inappropriate profile + Ongepast profiel + report reason + Incognito Incognito @@ -3248,6 +4309,11 @@ Dit kan niet ongedaan gemaakt worden! 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! @@ -3255,16 +4321,41 @@ Dit kan niet ongedaan gemaakt worden! No comment provided by engineer. - - Instantly - Direct - No comment provided by engineer. - Interface Interface No comment provided by engineer. + + Interface colors + Interface kleuren + No comment provided by engineer. + + + 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) + token status text + Invalid QR code Ongeldige QR-code @@ -3303,7 +4394,7 @@ Dit kan niet ongedaan gemaakt worden! Invalid server address! Ongeldig server adres! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4416,11 @@ Dit kan niet ongedaan gemaakt worden! 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 @@ -3337,17 +4433,17 @@ Dit kan niet ongedaan gemaakt worden! Irreversible message deletion is prohibited in this chat. - Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek. + 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. + 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. @@ -3366,6 +4462,11 @@ Dit kan niet ongedaan gemaakt worden! 3. 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 (%@). @@ -3383,8 +4484,8 @@ Dit kan niet ongedaan gemaakt worden! Join - Word lid van - No comment provided by engineer. + Word lid + swipe action Join group @@ -3426,6 +4527,11 @@ Dit is jouw link voor groep %@! Keep Bewaar + alert action + + + Keep conversation + Behoud het gesprek No comment provided by engineer. @@ -3436,7 +4542,7 @@ Dit is jouw link voor groep %@! Keep unused invitation? Ongebruikte uitnodiging bewaren? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4577,16 @@ Dit is jouw link voor groep %@! Leave Verlaten + swipe action + + + Leave chat + Chat verlaten + No comment provided by engineer. + + + Leave chat? + Chat verlaten? No comment provided by engineer. @@ -3513,6 +4629,21 @@ Dit is jouw link voor groep %@! Gelinkte desktops No comment provided by engineer. + + List + Lijst + swipe action + + + 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 message! Live bericht! @@ -3523,11 +4654,6 @@ Dit is jouw link voor groep %@! Live berichten No comment provided by engineer. - - Local - Lokaal - No comment provided by engineer. - Local name Lokale naam @@ -3548,11 +4674,6 @@ Dit is jouw link voor groep %@! 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 @@ -3563,21 +4684,11 @@ Dit is jouw link voor groep %@! 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 @@ -3603,11 +4714,36 @@ Dit is jouw link voor groep %@! Max 30 seconden, direct ontvangen. No comment provided by engineer. + + Media & file servers + Media- en bestandsservers + No comment provided by engineer. + + + Medium + Medium + blur media + Member Lid No comment provided by engineer. + + Member inactive + Lid inactief + item status text + + + Member reports + Ledenrapporten + chat feature + + + 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. @@ -3618,11 +4754,66 @@ Dit is jouw link voor groep %@! 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 SimpleX links. + Groepsleden kunnen SimpleX-links verzenden. + 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 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 delivery error Fout bij bezorging van bericht @@ -3633,11 +4824,31 @@ Dit is jouw link voor groep %@! Ontvangst bevestiging voor berichten! No comment provided by engineer. + + Message delivery warning + Waarschuwing voor berichtbezorging + item status text + Message draft Concept bericht No comment provided by engineer. + + Message forwarded + Bericht doorgestuurd + item status text + + + Message may be delivered later if member becomes active. + Het bericht kan later worden bezorgd als het lid actief wordt. + item status description + + + Message queue info + Informatie over berichtenwachtrij + No comment provided by engineer. + Message reactions Reacties op berichten @@ -3645,12 +4856,27 @@ Dit is jouw link voor groep %@! Message reactions are prohibited in this chat. - Reacties op berichten zijn verboden in deze 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. + 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. @@ -3658,6 +4884,16 @@ Dit is jouw link voor groep %@! Berichtbron blijft privé. No comment provided by engineer. + + Message status + Berichtstatus + No comment provided by engineer. + + + Message status: %@ + Berichtstatus: %@ + copied message info + Message text Bericht tekst @@ -3683,6 +4919,26 @@ Dit is jouw link voor groep %@! Berichten van %@ worden getoond! No comment provided by engineer. + + Messages in this chat will never be deleted. + Berichten in deze chat zullen nooit worden verwijderd. + alert message + + + Messages received + Berichten ontvangen + No comment provided by engineer. + + + Messages sent + Berichten verzonden + No comment provided by engineer. + + + Messages were deleted after you selected them. + Berichten zijn verwijderd nadat u ze had geselecteerd. + alert message + 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. @@ -3740,7 +4996,7 @@ Dit is jouw link voor groep %@! 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). + 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. @@ -3748,9 +5004,9 @@ Dit is jouw link voor groep %@! Migratie is voltooid No comment provided by engineer. - - Migrations: %@ - Migraties: %@ + + Migrations: + Migraties: No comment provided by engineer. @@ -3768,6 +5024,11 @@ Dit is jouw link voor groep %@! Gemodereerd op: %@ copied message info + + More + Meer + swipe action + More improvements are coming soon! Meer verbeteringen volgen snel! @@ -3778,25 +5039,30 @@ Dit is jouw link voor groep %@! Betrouwbaardere netwerkverbinding. No comment provided by engineer. + + More reliable notifications + Betrouwbaardere meldingen + No comment provided by engineer. + Most likely this connection is deleted. Hoogstwaarschijnlijk is deze verbinding verwijderd. item status description - - Most likely this contact has deleted the connection with you. - Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd. - No comment provided by engineer. - Multiple chat profiles - Meerdere chat profielen + Meerdere chatprofielen No comment provided by engineer. Mute Dempen - No comment provided by engineer. + notification label action + + + Mute all + Alles dempen + notification label action Muted when inactive! @@ -3806,7 +5072,7 @@ Dit is jouw link voor groep %@! Name Naam - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +5084,26 @@ Dit is jouw link voor groep %@! Netwerkverbinding No comment provided by engineer. + + Network decentralization + Netwerk decentralisatie + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden. + snd error text + Network management Netwerkbeheer No comment provided by engineer. + + Network operator + Netwerkbeheerder + No comment provided by engineer. + Network settings Netwerk instellingen @@ -3833,16 +5114,36 @@ Dit is jouw link voor groep %@! Netwerk status No comment provided by engineer. + + New + Nieuw + token status text + New Passcode Nieuwe toegangscode 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. + No comment provided by engineer. + New chat Nieuw gesprek No comment provided by engineer. + + New chat experience 🎉 + Nieuwe chatervaring 🎉 + No comment provided by engineer. + New contact request Nieuw contactverzoek @@ -3853,11 +5154,6 @@ Dit is jouw link voor groep %@! Nieuw contact: notification - - New database archive - Nieuw database archief - No comment provided by engineer. - New desktop app! Nieuwe desktop app! @@ -3868,11 +5164,21 @@ Dit is jouw link voor groep %@! Nieuwe weergavenaam No comment provided by engineer. + + New events + Nieuwe gebeurtenissen + notification + 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 @@ -3888,6 +5194,11 @@ Dit is jouw link voor groep %@! Nieuw wachtwoord… No comment provided by engineer. + + New server + Nieuwe server + No comment provided by engineer. + No Nee @@ -3898,6 +5209,21 @@ Dit is jouw link voor groep %@! Geen app wachtwoord Authentication unavailable + + 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 @@ -3918,9 +5244,14 @@ Dit is jouw link voor groep %@! Geen apparaattoken! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder. + item status description + No filtered chats - Geen gefilterde gesprekken + Geen gefilterde chats No comment provided by engineer. @@ -3933,26 +5264,111 @@ Dit is jouw link voor groep %@! Geen geschiedenis No comment provided by engineer. + + No info, try to reload + Geen info, probeer opnieuw te laden + No comment provided by engineer. + + + No media & file servers. + Geen media- en bestandsservers. + servers error + + + No message + Geen bericht + No comment provided by engineer. + + + No message servers. + Geen berichtenservers. + servers error + 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 No comment provided by engineer. + + 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. + servers error + + + No token! + Geen token! + alert title + + + 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 + No comment provided by engineer. + + + Nothing to forward! + Niets om door te sturen! + alert title + Notifications Meldingen @@ -3963,6 +5379,21 @@ Dit is jouw link voor groep %@! Meldingen zijn uitgeschakeld! No comment provided by engineer. + + Notifications error + Meldingsfout + alert title + + + Notifications privacy + Privacy van meldingen + No comment provided by engineer. + + + Notifications status + Meldingsstatus + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5411,35 @@ Dit is jouw link voor groep %@! Off Uit - No comment provided by engineer. + blur media Ok OK - No comment provided by engineer. + alert button Old database Oude database No comment provided by engineer. - - Old database archive - Oud database archief - No comment provided by engineer. - 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. +Requires compatible VPN. + Onion hosts zullen nodig zijn voor verbinding. +Vereist 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. +Requires compatible VPN. + Onion hosts worden gebruikt indien beschikbaar. +Vereist het inschakelen van VPN. No comment provided by engineer. @@ -4017,11 +5447,21 @@ Dit is jouw link voor groep %@! 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**. + + 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. @@ -4037,14 +5477,24 @@ Dit is jouw link voor groep %@! Alleen groep eigenaren kunnen spraak berichten inschakelen. No comment provided by engineer. + + Only sender and moderators see it + Alleen de verzender en moderators zien het + No comment provided by engineer. + + + 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 berichtreacties toevoegen. + 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 onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering). (24 uur) + Alleen jij kunt berichten definitief verwijderen (je contact kan ze markeren voor verwijdering). (24 uur) No comment provided by engineer. @@ -4064,7 +5514,7 @@ Dit is jouw link voor groep %@! Only your contact can add message reactions. - Alleen uw contact kan berichtreacties toevoegen. + Alleen uw contact kan bericht reacties toevoegen. No comment provided by engineer. @@ -4090,13 +5540,18 @@ Dit is jouw link voor groep %@! Open Open - No comment provided by engineer. + alert action Open Settings Open instellingen No comment provided by engineer. + + Open changes + Wijzigingen openen + No comment provided by engineer. + Open chat Chat openen @@ -4107,31 +5562,45 @@ Dit is jouw link voor groep %@! Chat console openen authentication reason + + Open conditions + Open voorwaarden + No comment provided by engineer. + Open group Open groep No comment provided by engineer. + + Open link? + alert title + Open migration to another device Open de migratie naar een ander apparaat authentication reason - - Open user profiles - Gebruikers profielen openen - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Open-source protocol en code. Iedereen kan de servers draaien. - No comment provided by engineer. - Opening app… App openen… No comment provided by engineer. + + Operator + Operator + No comment provided by engineer. + + + Operator server + Operatorserver + alert title + + + Or import archive file + Of importeer archiefbestand + No comment provided by engineer. + Or paste archive link Of plak de archief link @@ -4152,11 +5621,28 @@ Dit is jouw link voor groep %@! 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 Ander No comment provided by engineer. + + Other file errors: +%@ + Andere bestandsfouten: +%@ + alert message + PING count PING count @@ -4192,6 +5678,11 @@ Dit is jouw link voor groep %@! Toegangscode ingesteld! No comment provided by engineer. + + Password + Wachtwoord + No comment provided by engineer. + Password to show Wachtwoord om weer te geven @@ -4222,13 +5713,13 @@ Dit is jouw link voor groep %@! Plak de link die je hebt ontvangen 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 + + Periodic Periodiek No comment provided by engineer. @@ -4242,11 +5733,28 @@ Dit is jouw link voor groep %@! Beeld-in-beeld oproepen 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. +Please 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. +Deel 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. @@ -4306,7 +5814,7 @@ Fout: %@ 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. + Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chats. No comment provided by engineer. @@ -4314,11 +5822,31 @@ Fout: %@ Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u het kwijtraakt. No comment provided by engineer. + + 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. + token info + Polish interface Poolse interface No comment provided by engineer. + + Port + Poort + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Mogelijk is de certificaat vingerafdruk in het server adres onjuist @@ -4329,41 +5857,86 @@ Fout: %@ Bewaar het laatste berichtconcept, met bijlagen. No comment provided by engineer. - - Preset server - Vooraf ingestelde server - No comment provided by engineer. - Preset server address Vooraf ingesteld server adres No comment provided by engineer. + + 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🚀 + No comment provided by engineer. + Private notes Privé notities name of notes to self + + 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 @@ -4379,25 +5952,20 @@ Fout: %@ Profiel afbeeldingen No comment provided by engineer. - - Profile name - Profielnaam - No comment provided by engineer. - - - Profile name: - Profielnaam: - No comment provided by engineer. - Profile password Profiel wachtwoord No comment provided by engineer. + + Profile theme + Profiel thema + No comment provided by engineer. + Profile update will be sent to your contacts. Profiel update wordt naar uw contacten verzonden. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4406,12 +5974,12 @@ Fout: %@ Prohibit irreversible message deletion. - Verbied het onomkeerbaar verwijderen van berichten. + Verbied het definitief verwijderen van berichten. No comment provided by engineer. Prohibit message reactions. - Berichtreacties verbieden. + Bericht reacties verbieden. No comment provided by engineer. @@ -4419,6 +5987,11 @@ Fout: %@ 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 SimpleX links. Verbied het verzenden van SimpleX-links @@ -4444,14 +6017,26 @@ Fout: %@ Verbieden het verzenden van spraak berichten. No comment provided by engineer. + + Protect IP address + Bescherm het IP-adres + No comment provided by engineer. + Protect app screen App scherm verbergen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen. +Schakel dit in in *Netwerk en servers*-instellingen. + No comment provided by engineer. + Protect your chat profiles with a password! - Bescherm je chat profielen met een wachtwoord! + Bescherm je chatprofielen met een wachtwoord! No comment provided by engineer. @@ -4464,6 +6049,21 @@ Fout: %@ 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 @@ -4484,6 +6084,11 @@ Fout: %@ Beoordeel de app No comment provided by engineer. + + Reachable chat toolbar + Toegankelijke chatwerkbalk + No comment provided by engineer. + React… Reageer… @@ -4492,33 +6097,28 @@ Fout: %@ Read Lees - No comment provided by engineer. + swipe action 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). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Lees meer in onze GitHub repository. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Lees meer in onze [GitHub-repository](https://github.com/simplex-chat/simplex-chat#readme). @@ -4529,6 +6129,11 @@ Fout: %@ Bevestigingen zijn uitgeschakeld No comment provided by engineer. + + Receive errors + Fouten ontvangen + No comment provided by engineer. + Received at Ontvangen op @@ -4549,16 +6154,26 @@ Fout: %@ Ontvangen bericht message info title + + 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. No comment provided by engineer. - - Receiving concurrency - Gelijktijdig ontvangen - No comment provided by engineer. - Receiving file will be stopped. Het ontvangen van het bestand wordt gestopt. @@ -4584,11 +6199,36 @@ Fout: %@ 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? @@ -4609,10 +6249,26 @@ Fout: %@ Verminderd batterijgebruik No comment provided by engineer. + + Register + Register + No comment provided by engineer. + + + Register notification token? + Meldingstoken registreren? + token info + + + Registered + Geregistreerd + token status text + Reject Afwijzen - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4639,6 +6295,16 @@ Fout: %@ 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 @@ -4699,6 +6365,56 @@ Fout: %@ Antwoord chat item action + + Report + rapporteren + chat item action + + + 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. + report reason + + + Report reason? + Reden melding? + No comment provided by engineer. + + + 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 reason + + + Report: %@ + rapporteer: %@ + report in notification + + + Reporting messages to moderators is prohibited. + Het is niet toegestaan om berichten aan moderators te melden. + No comment provided by engineer. + + + Reports + Rapporten + No comment provided by engineer. + Required Vereist @@ -4709,19 +6425,44 @@ Fout: %@ 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. + + 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 chat profiel aan te maken + Start de app opnieuw om een nieuw chatprofiel aan te maken No comment provided by engineer. @@ -4759,9 +6500,9 @@ Fout: %@ Onthullen chat item action - - Revert - Terugdraaien + + Review conditions + Voorwaarden bekijken No comment provided by engineer. @@ -4789,9 +6530,19 @@ Fout: %@ Chat uitvoeren No comment provided by engineer. - - SMP servers - SMP servers + + SMP server + SMP server + No comment provided by engineer. + + + SOCKS proxy + SOCKS proxy + No comment provided by engineer. + + + Safely receive files + Veilig bestanden ontvangen No comment provided by engineer. @@ -4802,46 +6553,47 @@ Fout: %@ Save Opslaan - chat item action + alert button +chat item action 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. + alert button 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 list + Lijst opslaan + No comment provided by engineer. + Save passphrase and open chat - Bewaar het wachtwoord en open je gesprekken + Wachtwoord opslaan en open je chats No comment provided by engineer. @@ -4852,7 +6604,7 @@ Fout: %@ Save preferences? Voorkeuren opslaan? - No comment provided by engineer. + alert title Save profile password @@ -4861,24 +6613,24 @@ Fout: %@ Save servers - Bewaar servers + Servers opslaan No comment provided by engineer. Save servers? Servers opslaan? - No comment provided by engineer. - - - Save settings? - Instellingen opslaan? - No comment provided by engineer. + alert title Save welcome message? - Welkomst bericht opslaan? + Welkom bericht opslaan? No comment provided by engineer. + + Save your profile? + Uw profiel opslaan? + alert title + Saved Opgeslagen @@ -4899,6 +6651,21 @@ Fout: %@ Opgeslagen bericht message info title + + 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 QR code Scan QR-code @@ -4939,11 +6706,21 @@ Fout: %@ Zoeken of plak een SimpleX link No comment provided by engineer. + + Secondary + Secundair + No comment provided by engineer. + Secure queue Veilige wachtrij server test step + + Secured + Beveiligd + No comment provided by engineer. + Security assessment Beveiligingsbeoordeling @@ -4957,6 +6734,21 @@ Fout: %@ Select Selecteer + chat item action + + + 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. @@ -4994,11 +6786,6 @@ Fout: %@ Stuur ontvangstbewijzen naar 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 @@ -5009,6 +6796,11 @@ Fout: %@ Stuur een verdwijnend bericht No comment provided by engineer. + + Send errors + Verzend fouten + No comment provided by engineer. + Send link previews Link voorbeelden verzenden @@ -5019,14 +6811,29 @@ Fout: %@ 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. @@ -5052,7 +6859,7 @@ Fout: %@ Sender cancelled file transfer. Afzender heeft bestandsoverdracht geannuleerd. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6916,11 @@ Fout: %@ Verzonden op: %@ copied message info + + Sent directly + Direct verzonden + No comment provided by engineer. + Sent file event Verzonden bestandsgebeurtenis @@ -5119,11 +6931,71 @@ Fout: %@ Verzonden bericht message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + Server toegevoegd aan operator %@. + alert message + + + Server address + Server adres + No comment provided by engineer. + + + Server address is incompatible with network settings. + Serveradres is niet compatibel met netwerkinstellingen. + srv error text. + + + Server address is incompatible with network settings: %@. + Serveradres is incompatibel met netwerkinstellingen: %@. + No comment provided by engineer. + + + Server operator changed. + Serveroperator gewijzigd. + alert title + + + Server operators + Serverbeheerders + No comment provided by engineer. + + + Server protocol changed. + Serverprotocol gewijzigd. + alert title + Server requires authorization to create queues, check password Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord @@ -5139,11 +7011,36 @@ Fout: %@ Servertest mislukt! No comment provided by engineer. + + Server type + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + Serverversie is incompatibel met netwerkinstellingen. + srv error text + + + 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 @@ -5154,11 +7051,21 @@ Fout: %@ 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 @@ -5169,6 +7076,11 @@ Fout: %@ Stel het in in plaats van systeemverificatie. No comment provided by engineer. + + Set message expiration in chats. + Stel de berichtvervaldatum in chats in. + No comment provided by engineer. + Set passcode Toegangscode instellen @@ -5199,6 +7111,11 @@ Fout: %@ Instellingen No comment provided by engineer. + + Settings were changed. + Instellingen zijn gewijzigd. + alert message + Shape profile images Vorm profiel afbeeldingen @@ -5207,21 +7124,42 @@ Fout: %@ Share Deel - chat item action + alert action +chat item action 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 SimpleX address on social media. + Deel het SimpleX-adres op sociale media. + No comment provided by engineer. + Share address Adres delen No comment provided by engineer. + + Share address publicly + Adres openbaar delen + No comment provided by engineer. + Share address with contacts? Adres delen met contacten? + alert title + + + Share from other apps. + Delen vanuit andere apps. No comment provided by engineer. @@ -5229,16 +7167,30 @@ Fout: %@ Deel link No comment provided by engineer. + + Share profile + Profiel delen + 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. + + Short link + No comment provided by engineer. + Show QR code Toon QR-code @@ -5259,21 +7211,46 @@ Fout: %@ 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 → on messages sent via private routing. + Toon → bij berichten verzonden via privéroutering. + 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 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. @@ -5304,6 +7281,20 @@ Fout: %@ 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 channel link + simplex link type + SimpleX contact address SimpleX contact adres @@ -5324,9 +7315,9 @@ Fout: %@ SimpleX links chat feature - - SimpleX links are prohibited in this group. - SimpleX-links zijn in deze groep verboden. + + SimpleX links are prohibited. + SimpleX-links zijn niet toegestaan. No comment provided by engineer. @@ -5339,11 +7330,21 @@ Fout: %@ Eenmalige SimpleX uitnodiging simplex link type + + 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 @@ -5359,16 +7360,49 @@ Fout: %@ Kleine groepen (max 20) No comment provided by engineer. + + Soft + Soft + blur media + + + 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + Sommige servers zijn niet geslaagd voor de test: +%@ + alert message + Somebody Iemand notification title + + Spam + Spam + blocking reason +report reason + Square, circle, or anything in between. Vierkant, cirkel of iets daartussenin. @@ -5389,6 +7423,16 @@ Fout: %@ Start migratie No comment provided by engineer. + + Starting from %@. + Beginnend vanaf %@. + No comment provided by engineer. + + + Statistics + Statistieken + No comment provided by engineer. + Stop Stop @@ -5404,11 +7448,6 @@ Fout: %@ Stop chat No comment provided by engineer. - - Stop chat to enable database actions - Stop de chat om database acties mogelijk te maken - 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. @@ -5437,21 +7476,46 @@ Fout: %@ Stop sharing Stop met delen - No comment provided by engineer. + alert action Stop sharing address? Stop met het delen van adres? - No comment provided by engineer. + alert title Stopping chat Chat stoppen No comment provided by engineer. + + Storage + Opslag + No comment provided by engineer. + + + Strong + Krachtig + blur media + Submit - Indienen + 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. @@ -5459,6 +7523,16 @@ Fout: %@ 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 @@ -5469,11 +7543,21 @@ Fout: %@ Systeem authenticatie 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 @@ -5489,11 +7573,21 @@ Fout: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Staart + No comment provided by engineer. + Take picture Foto nemen No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + Tik op SimpleX-adres maken in het menu om het later te maken. + No comment provided by engineer. + Tap button Tik op de knop @@ -5501,44 +7595,49 @@ Fout: %@ Tap to Connect - Tik om verbinding te maken + Tik hier om verbinding te maken No comment provided by engineer. Tap to activate profile. - Tik om profiel te activeren. + Tik hier om profiel te activeren. No comment provided by engineer. Tap to join - Tik om lid te worden + Tik hier om lid te worden No comment provided by engineer. Tap to join incognito - Tik om incognito lid te worden + Tik hier om incognito lid te worden No comment provided by engineer. Tap to paste link - Tik om de link te plakken + Tik hier om de link te plakken No comment provided by engineer. Tap to scan - Tik om te scannen + Tik hier om te scannen No comment provided by engineer. - - Tap to start a new chat - Tik om een nieuw gesprek te starten - No comment provided by engineer. + + Temporary file error + Tijdelijke bestandsfout + file error alert title Test failed at step %@. Test mislukt bij stap %@. server test failure + + Test notifications + Testmeldingen + No comment provided by engineer. + Test server Server test @@ -5552,7 +7651,7 @@ Fout: %@ Tests failed! Testen mislukt! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7668,6 @@ Fout: %@ 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 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. @@ -5586,6 +7680,16 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. 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. @@ -5596,6 +7700,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De code die u heeft gescand is geen SimpleX link QR-code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, 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! @@ -5616,6 +7725,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De versleuteling werkt en de nieuwe versleutelingsovereenkomst is niet vereist. Dit kan leiden tot verbindingsfouten! No comment provided by engineer. + + The future of messaging + De volgende generatie privéberichten + No comment provided by engineer. + The hash of the previous message is different. De hash van het vorige bericht is anders. @@ -5631,9 +7745,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. 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. @@ -5641,9 +7760,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd. No comment provided by engineer. - - The profile is only shared with your contacts. - Het profiel wordt alleen gedeeld met uw contacten. + + 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. @@ -5658,7 +7782,12 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. The servers for new connections of your current chat profile **%@**. - De servers voor nieuwe verbindingen van uw huidige chat profiel **%@**. + De servers voor nieuwe verbindingen van uw huidige chatprofiel **%@**. + No comment provided by engineer. + + + The servers for new files of your current chat profile **%@**. + De servers voor nieuwe bestanden van uw huidige chatprofiel **%@**. No comment provided by engineer. @@ -5666,9 +7795,19 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De tekst die u hebt geplakt is geen SimpleX link. No comment provided by engineer. - - Theme - Thema + + 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. @@ -5691,9 +7830,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren. No comment provided by engineer. + + 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. + alert message + 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. + Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren. No comment provided by engineer. @@ -5708,7 +7852,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. This device name - Deze apparaatnaam + Naam van dit apparaat No comment provided by engineer. @@ -5736,9 +7880,28 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Dit is uw eigen eenmalige link! 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. + No comment provided by engineer. + + + 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 chat profiel **%@**. + Deze instelling is van toepassing op berichten in je huidige chatprofiel **%@**. + No comment provided by engineer. + + + Title + Titel No comment provided by engineer. @@ -5761,9 +7924,9 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. 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. @@ -5771,6 +7934,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,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. 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. @@ -5785,7 +7973,12 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc 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**. + 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. @@ -5793,21 +7986,51 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc 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. No comment provided by engineer. + + Token status: %@. + Tokenstatus: %@. + token status + + + 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: %@). @@ -5863,10 +8086,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Lid deblokkeren? No comment provided by engineer. - - Unexpected error: %@ - Onverwachte fout: %@ - item status description + + Undelivered messages + Niet afgeleverde berichten + No comment provided by engineer. Unexpected migration state @@ -5876,7 +8099,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Unfav. Niet fav. - No comment provided by engineer. + swipe action Unhide @@ -5885,7 +8108,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Unhide chat profile - Chat profiel zichtbaar maken + Chatprofiel zichtbaar maken No comment provided by engineer. @@ -5913,6 +8136,11 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Onbekende fout No comment provided by engineer. + + Unknown servers! + Onbekende servers! + alert title + 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. @@ -5948,11 +8176,15 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Unmute Dempen opheffen - No comment provided by engineer. + notification label action Unread Ongelezen + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5965,11 +8197,6 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Update No comment provided by engineer. - - Update .onion hosts setting? - .onion hosts-instelling updaten? - No comment provided by engineer. - Update database passphrase Database wachtwoord bijwerken @@ -5980,9 +8207,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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 No comment provided by engineer. @@ -5990,16 +8222,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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. - No comment provided by engineer. - Upgrade and open chat Upgrade en open chat No comment provided by engineer. + + Upload errors + Upload fouten + No comment provided by engineer. + Upload failed Upload mislukt @@ -6010,21 +8242,50 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Upload bestand server test step + + 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 %@ + Gebruik %@ + No comment provided by engineer. + Use .onion hosts Gebruik .onion-hosts No comment provided by engineer. + + Use SOCKS proxy + Gebruik SOCKS proxy + No comment provided by engineer. + Use SimpleX Chat servers? SimpleX Chat servers gebruiken? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + Gebruik TCP-poort %@ als er geen poort is opgegeven. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Gebruik chat @@ -6035,6 +8296,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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 @@ -6060,29 +8331,58 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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 short links (BETA) + No comment provided by engineer. + Use the app while in the call. Gebruik de app tijdens het gesprek. No comment provided by engineer. - - User profile - Gebruikers profiel + + Use the app with one hand. + Gebruik de app met één hand. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Het gebruik van .onion-hosts vereist een compatibele VPN-provider. + + 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. - SimpleX Chat servers gebruiken. + Gebruik SimpleX Chat servers. No comment provided by engineer. @@ -6150,11 +8450,21 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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 + No comment provided by engineer. + Visible history Zichtbare geschiedenis @@ -6167,12 +8477,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Voice messages are prohibited in this chat. - Spraak berichten zijn verboden in deze 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. @@ -6182,7 +8492,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Voice messages prohibited! - Spraak berichten verboden! + Spraak berichten niet toegestaan! No comment provided by engineer. @@ -6210,6 +8520,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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. + 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 @@ -6232,12 +8552,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Welcome message - Welkomst bericht + Welkom bericht No comment provided by engineer. Welcome message is too long - Welkomstbericht is te lang + Welkom bericht is te lang No comment provided by engineer. @@ -6255,9 +8575,9 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Bij het verbinden van audio- en video-oproepen. 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 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. @@ -6287,7 +8607,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak With optional welcome message. - Met optioneel welkomst bericht. + Met optioneel welkom bericht. No comment provided by engineer. @@ -6295,24 +8615,39 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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. + No comment provided by engineer. + + + 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: %@. + alert message + Wrong database passphrase Verkeerd wachtwoord voor de database No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd. + file error text + Wrong passphrase! Verkeerd wachtwoord! No comment provided by engineer. - - XFTP servers - XFTP servers - No comment provided by engineer. - - - You - Jij + + XFTP server + XFTP server No comment provided by engineer. @@ -6332,7 +8667,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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. + Je hebt al een chatprofiel met dezelfde weergave naam. Kies een andere naam. No comment provided by engineer. @@ -6340,6 +8675,11 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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 %@. @@ -6387,11 +8727,26 @@ Deelnameverzoek herhalen? Je bent uitgenodigd voor de groep No comment provided by engineer. + + 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 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 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 @@ -6422,11 +8777,21 @@ Deelnameverzoek herhalen? Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Je kunt nu berichten sturen naar %@ notification body + + 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. @@ -6442,16 +8807,16 @@ Deelnameverzoek herhalen? 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. - 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 No comment provided by engineer. + + 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. @@ -6465,23 +8830,23 @@ Deelnameverzoek herhalen? You can view invitation link again in connection details. U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails. - No comment provided by engineer. + alert message You can't send messages! Je kunt geen berichten versturen! 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. - No comment provided by engineer. - You could not be verified; please try again. U kon niet worden geverifieerd; probeer het opnieuw. No comment provided by engineer. + + You decide who can connect. + Jij bepaalt wie er verbinding mag maken. + No comment provided by engineer. + You have already requested connection via this address! U heeft al een verbinding aangevraagd via dit adres! @@ -6494,11 +8859,6 @@ Repeat connection request? Verbindingsverzoek 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. @@ -6519,11 +8879,26 @@ Verbindingsverzoek herhalen? Je bent lid geworden van deze groep. Verbinding maken met uitnodigend groepslid. 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. @@ -6539,6 +8914,11 @@ Verbindingsverzoek herhalen? Je hebt een groep uitnodiging verzonden No comment provided by engineer. + + You should receive notifications. + U zou meldingen moeten ontvangen. + token info + 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! @@ -6574,6 +8954,11 @@ Verbindingsverzoek herhalen? U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn. No comment provided by engineer. + + You will stop receiving messages from this 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. @@ -6594,31 +8979,16 @@ Verbindingsverzoek herhalen? 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 ICE servers Uw ICE servers No comment provided by engineer. - - Your SMP servers - Uw SMP servers - No comment provided by engineer. - Your SimpleX address Uw SimpleX adres No comment provided by engineer. - - Your XFTP servers - Uw XFTP servers - No comment provided by engineer. - Your calls Uw oproepen @@ -6634,16 +9004,19 @@ Verbindingsverzoek herhalen? Uw chat database is niet versleuteld, stel een wachtwoord in om deze te versleutelen. No comment provided by engineer. + + Your chat preferences + Uw chat voorkeuren + alert title + Your chat profiles Uw chat profielen No comment provided by engineer. - - 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). - 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. + + 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. @@ -6661,6 +9034,11 @@ U kunt deze verbinding verbreken en het contact verwijderen en later proberen me Uw contacten blijven verbonden. No comment provided by engineer. + + 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. @@ -6691,33 +9069,36 @@ U kunt deze verbinding verbreken en het contact verwijderen en later proberen me Uw profiel **%@** wordt gedeeld. No comment provided by engineer. - - 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. + + 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. - - Your profile, contacts and delivered messages are stored on your device. - Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen. + + 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. No comment provided by engineer. + + 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. + alert message + Your random profile Je willekeurige profiel No comment provided by engineer. - - Your server - Uw server - No comment provided by engineer. - Your server address Uw server adres No comment provided by engineer. + + Your servers + Uw servers + No comment provided by engineer. + Your settings Uw instellingen @@ -6758,6 +9139,11 @@ SimpleX servers kunnen uw profiel niet zien. geaccepteerde oproep call status + + accepted invitation + geaccepteerde uitnodiging + chat list item title + admin Beheerder @@ -6793,6 +9179,16 @@ SimpleX servers kunnen uw profiel niet zien. en %lld andere gebeurtenissen No comment provided by engineer. + + archived report + gearchiveerd rapport + No comment provided by engineer. + + + attempts + pogingen + No comment provided by engineer. + audio call (not e2e encrypted) audio oproep (niet e2e versleuteld) @@ -6820,19 +9216,25 @@ SimpleX servers kunnen uw profiel niet zien. blocked %@ - geblokkeerd %@ + blokkeerde %@ rcv group event chat item blocked by admin geblokkeerd door beheerder - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold vetgedrukt No comment provided by engineer. + + call + bellen + No comment provided by engineer. + call error oproepfout @@ -6936,7 +9338,7 @@ SimpleX servers kunnen uw profiel niet zien. connecting… Verbinden… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +9385,16 @@ SimpleX servers kunnen uw profiel niet zien. dagen time unit + + decryption errors + decoderingsfouten + No comment provided by engineer. + default (%@) standaard (%@) - pref value + delete after time +pref value default (no) @@ -7033,6 +9441,11 @@ SimpleX servers kunnen uw profiel niet zien. dubbel bericht integrity error chat item + + duplicates + duplicaten + No comment provided by engineer. + e2e encrypted e2e versleuteld @@ -7108,9 +9521,9 @@ SimpleX servers kunnen uw profiel niet zien. fout No comment provided by engineer. - - event happened - gebeurtenis gebeurd + + expired + verlopen No comment provided by engineer. @@ -7143,6 +9556,11 @@ SimpleX servers kunnen uw profiel niet zien. 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. + + inactive + inactief + No comment provided by engineer. + incognito via contact address link incognito via contact adres link @@ -7183,6 +9601,11 @@ SimpleX servers kunnen uw profiel niet zien. uitnodiging voor groep %@ group name + + invite + uitnodiging + No comment provided by engineer. + invited uitgenodigd @@ -7238,6 +9661,11 @@ SimpleX servers kunnen uw profiel niet zien. is toegetreden rcv group event chat item + + message + bericht + No comment provided by engineer. + message received bericht ontvangen @@ -7263,6 +9691,11 @@ SimpleX servers kunnen uw profiel niet zien. gemodereerd door %@ marked deleted chat item preview text + + moderator + moderator + member role + months maanden @@ -7271,7 +9704,7 @@ SimpleX servers kunnen uw profiel niet zien. never nooit - No comment provided by engineer. + delete after time new message @@ -7302,8 +9735,8 @@ SimpleX servers kunnen uw profiel niet zien. off uit enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9753,16 @@ SimpleX servers kunnen uw profiel niet zien. aan group pref value + + other + overig + No comment provided by engineer. + + + other errors + overige fouten + No comment provided by engineer. + owner Eigenaar @@ -7335,6 +9778,16 @@ SimpleX servers kunnen uw profiel niet zien. peer-to-peer No comment provided by engineer. + + pending + In behandeling + No comment provided by engineer. + + + pending approval + in afwachting van goedkeuring + No comment provided by engineer. + quantum resistant e2e encryption quantum bestendige e2e-codering @@ -7350,6 +9803,11 @@ SimpleX servers kunnen uw profiel niet zien. bevestiging ontvangen… No comment provided by engineer. + + rejected + afgewezen + No comment provided by engineer. + rejected call geweigerde oproep @@ -7380,6 +9838,11 @@ SimpleX servers kunnen uw profiel niet zien. heeft je verwijderd rcv group event chat item + + requested to connect + verzocht om verbinding te maken + chat list item title + saved opgeslagen @@ -7390,6 +9853,11 @@ SimpleX servers kunnen uw profiel niet zien. opgeslagen van %@ No comment provided by engineer. + + search + zoekopdracht + No comment provided by engineer. + sec sec @@ -7415,6 +9883,15 @@ SimpleX servers kunnen uw profiel niet zien. stuur een direct bericht No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + informatie over serverwachtrij: %1$@ + +laatst ontvangen bericht: %2$@ + queue info + set new contact address nieuw contactadres instellen @@ -7422,7 +9899,7 @@ SimpleX servers kunnen uw profiel niet zien. set new profile picture - nieuwe profielfoto instellen + nieuwe profielfoto profile update event chat item @@ -7455,11 +9932,21 @@ SimpleX servers kunnen uw profiel niet zien. onbekend connection info + + unknown servers + onbekende relays + No comment provided by engineer. + unknown status onbekende status No comment provided by engineer. + + unprotected + onbeschermd + No comment provided by engineer. + updated group profile bijgewerkt groep profiel @@ -7500,6 +9987,11 @@ SimpleX servers kunnen uw profiel niet zien. via relay No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) video gesprek (niet e2e versleuteld) @@ -7525,6 +10017,11 @@ SimpleX servers kunnen uw profiel niet zien. weken time unit + + when IP hidden + wanneer IP verborgen is + No comment provided by engineer. + yes Ja @@ -7542,7 +10039,7 @@ SimpleX servers kunnen uw profiel niet zien. you are observer - jij bent waarnemer + je bent waarnemer No comment provided by engineer. @@ -7572,7 +10069,7 @@ SimpleX servers kunnen uw profiel niet zien. you left - jij bent vertrokken + je bent vertrokken snd group event chat item @@ -7609,7 +10106,7 @@ SimpleX servers kunnen uw profiel niet zien.
- +
@@ -7646,7 +10143,7 @@ SimpleX servers kunnen uw profiel niet zien.
- +
@@ -7666,4 +10163,249 @@ SimpleX servers kunnen uw profiel niet zien.
+ +
+ +
+ + + %d new events + ‐%d nieuwe gebeurtenissen + notification body + + + From %d chat(s) + notification body + + + From: %@ + Van: %@ + notification body + + + New events + Nieuwe gebeurtenissen + notification + + + New messages + Nieuwe berichten + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 d71bd5843a..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 @@
- +
- - - - - - 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ć) @@ -127,9 +100,14 @@ %@ jest zweryfikowany No comment provided by engineer. + + %@ server + %@ serwer + No comment provided by engineer. + %@ servers - %@ serwery + %@ serwery/ów No comment provided by engineer. @@ -142,6 +120,11 @@ %@ chce się połączyć! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ i %lld członków @@ -162,11 +145,36 @@ %d dni time interval + + %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. + forward confirmation reason + %d hours %d godzin time interval + + %d messages not forwarded + %d wiadomości nie przekazanych + alert title + %d min %d min @@ -182,6 +190,11 @@ %d sek time interval + + %d seconds(s) + %d sekundach + delete after time + %d skipped message(s) %d pominięte wiadomość(i) @@ -252,11 +265,6 @@ %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 @@ -307,11 +315,6 @@ %u pominiętych wiadomości. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (nowy) @@ -322,33 +325,23 @@ (to urządzenie v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **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. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Dodaj nowy kontakt**: aby stworzyć swój jednorazowy kod QR lub link dla kontaktu. - No comment provided by engineer. - **Create group**: to create a new group. **Utwórz grupę**: aby utworzyć nową grupę. 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. + + **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). + + **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. @@ -362,11 +355,16 @@ **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. + + **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 / 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ą zapisania kodu dostępu w Keychain. @@ -392,11 +390,6 @@ \*pogrubiony* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +426,6 @@ - historia edycji. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 sek @@ -451,7 +439,8 @@ 1 day 1 dzień - time interval + delete after time +time interval 1 hour @@ -466,12 +455,29 @@ 1 month 1 miesiąc - time interval + delete after time +time interval 1 week 1 tydzień - time interval + delete after time +time interval + + + 1 year + 1 roku + delete after time + + + 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 @@ -488,11 +494,6 @@ 30 sekund No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -542,31 +543,32 @@ Przerwać zmianę adresu? No comment provided by engineer. - - About SimpleX - O SimpleX - No comment provided by engineer. - About SimpleX Chat O SimpleX Chat No comment provided by engineer. - - About SimpleX address - O adresie SimpleX + + About operators + O operatorach No comment provided by engineer. - - Accent color - Kolor akcentu + + Accent + Akcent No comment provided by engineer. Accept Akceptuj accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Zaakceptuj warunki + No comment provided by engineer. Accept connection request? @@ -581,21 +583,47 @@ Accept incognito Akceptuj incognito - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Zaakceptowano warunki + No comment provided by engineer. + + + Acknowledged + Potwierdzono + No comment provided by engineer. + + + Acknowledgement errors + Błędy potwierdzenia + No comment provided by engineer. + + + Active + Aktywne + token status text + + + 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 contact - Dodaj kontakt + + Add friends + Dodaj znajomych No comment provided by engineer. - - Add preset servers - Dodaj gotowe serwery + + Add list + Dodaj listę No comment provided by engineer. @@ -603,14 +631,19 @@ Dodaj profil No comment provided by engineer. + + 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 server… - Dodaj serwer… + + Add team members + Dodaj członków zespołu No comment provided by engineer. @@ -618,11 +651,46 @@ 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 @@ -633,6 +701,16 @@ 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 + No comment provided by engineer. + Admins can block a member for all. Administratorzy mogą blokować członka dla wszystkich. @@ -648,6 +726,16 @@ Zaawansowane ustawienia sieci No comment provided by engineer. + + Advanced settings + Zaawansowane ustawienia + No comment provided by engineer. + + + All + Wszystko + No comment provided by engineer. + All app data is deleted. Wszystkie dane aplikacji są usunięte. @@ -658,16 +746,31 @@ Wszystkie czaty i wiadomości zostaną usunięte - nie można tego cofnąć! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + Wszystkie rozmowy zostaną usunięte z listy %@, a lista usunięta. + alert message + 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. 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ąć! @@ -683,6 +786,20 @@ Wszystkie nowe wiadomości z %@ zostaną ukryte! No comment provided by engineer. + + All profiles + Wszystkie profile + profile dropdown + + + All reports will be archived for you. + Wszystkie raporty zostaną dla Ciebie zarchiwizowane. + No comment provided by engineer. + + + All servers + No comment provided by engineer. + All your contacts will remain connected. Wszystkie Twoje kontakty pozostaną połączone. @@ -708,11 +825,21 @@ 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 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) @@ -738,11 +865,21 @@ Zezwól na wysyłanie znikających wiadomości. No comment provided by engineer. + + 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 SimpleX links. Zezwól na wysyłanie linków SimpleX. @@ -808,6 +945,11 @@ Już dołączono do grupy! 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 @@ -818,11 +960,21 @@ Tworzony jest pusty profil czatu o podanej nazwie, a aplikacja otwiera się jak zwykle. No comment provided by engineer. + + Another reason + Inny powód + report reason + 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: %@ @@ -838,6 +990,11 @@ 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 @@ -853,6 +1010,11 @@ 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 @@ -873,11 +1035,56 @@ 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? + No comment provided by engineer. + + + Archive reports + Archiwizuj zgłoszenia + swipe action + + + Archived contacts + Zarchiwizowane kontakty + No comment provided by engineer. + Archiving database Archiwizowanie bazy danych @@ -943,11 +1150,21 @@ Automatyczne akceptowanie obrazów No comment provided by engineer. + + Auto-accept settings + Ustawienia automatycznej akceptacji + alert title + Back Wstecz No comment provided by engineer. + + Background + Tło + No comment provided by engineer. + Bad desktop address Zły adres komputera @@ -963,16 +1180,59 @@ Zły hash 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 groups performance + 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 privacy and security + 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 @@ -1008,6 +1268,16 @@ 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. + Both you and your contact can add message reactions. Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości. @@ -1038,11 +1308,32 @@ 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. + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! Połączenie już zakończone! @@ -1053,11 +1344,26 @@ Połączenia No comment provided by engineer. + + 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! @@ -1068,10 +1374,16 @@ 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 + No comment provided by engineer. + Cancel Anuluj - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1395,20 @@ 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. + Cannot receive file Nie można odebrać pliku - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Przekroczono pojemność - odbiorca nie otrzymał wcześniej wysłanych wiadomości. + snd error text Cellular @@ -1098,6 +1420,15 @@ Zmień No comment provided by engineer. + + Change automatic message deletion? + alert title + + + Change chat profiles + Zmień profil czatu + authentication reason + Change database passphrase? Zmienić hasło bazy danych? @@ -1142,11 +1473,26 @@ Change self-destruct passcode Zmień pin samozniszczenia authentication reason - set passcode view +set passcode view - - 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. @@ -1164,9 +1510,14 @@ Baza danych czatu usunięta No comment provided by engineer. + + Chat database exported + Wyeksportowano bazę danych czatów + No comment provided by engineer. + Chat database imported - Zaimportowano bazę danych czatu + Zaimportowano bazę danych czatów No comment provided by engineer. @@ -1184,6 +1535,11 @@ 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! @@ -1194,15 +1550,50 @@ Preferencje czatu No comment provided by engineer. + + Chat preferences were changed. + Preferencje czatu zostały zmienione. + alert message + + + 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. + No comment provided by engineer. + Check server address and try again. Sprawdź adres serwera i spróbuj ponownie. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1615,25 @@ 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 + No comment provided by engineer. + Clear Wyczyść - No comment provided by engineer. + swipe action Clear conversation @@ -1239,6 +1645,14 @@ Wyczyścić rozmowę? No comment provided by engineer. + + Clear group? + No comment provided by engineer. + + + Clear or delete group? + No comment provided by engineer. + Clear private notes? Wyczyścić prywatne notatki? @@ -1249,11 +1663,20 @@ Wyczyść weryfikację No comment provided by engineer. - - Colors - Kolory + + Color chats with the new themes. + Koloruj czaty z nowymi motywami. No comment provided by engineer. + + Color mode + Tryb koloru + No comment provided by engineer. + + + Community guidelines violation + report reason + Compare file Porównaj plik @@ -1264,11 +1687,52 @@ Porównaj kody bezpieczeństwa ze swoimi kontaktami. 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. + + + 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 Skonfiguruj serwery ICE No comment provided by engineer. + + Configure server operators + No comment provided by engineer. + Confirm Potwierdź @@ -1279,11 +1743,21 @@ Potwierdź Pin 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 @@ -1309,6 +1783,10 @@ Potwierdź wgranie No comment provided by engineer. + + Confirmed + token status text + Connect Połącz @@ -1329,6 +1807,11 @@ Połącz do komputera 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ą? @@ -1368,16 +1851,31 @@ To jest twój jednorazowy link! Połącz z %@ No comment provided by engineer. + + Connected + Połączony + No comment provided by engineer. + Connected desktop Połączony komputer 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 to server… Łączenie z serwerem… @@ -1388,6 +1886,11 @@ To jest twój jednorazowy link! Łą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 @@ -1398,6 +1901,15 @@ To jest twój jednorazowy link! Połączenie No comment provided by engineer. + + Connection and servers status. + Stan połączenia i serwerów. + No comment provided by engineer. + + + Connection blocked + No comment provided by engineer. + Connection error Błąd połączenia @@ -1408,11 +1920,33 @@ To jest twój jednorazowy link! Błąd połączenia (UWIERZYTELNIANIE) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + No comment provided by engineer. + + + Connection not ready. + 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! No comment provided by engineer. + + Connection requires encryption renegotiation. + No comment provided by engineer. + + + Connection security + No comment provided by engineer. + Connection terminated Połączenie zakończone @@ -1423,6 +1957,16 @@ To jest twój jednorazowy link! Czas połączenia minął No comment provided by engineer. + + Connection with desktop stopped + Połączenie z komputerem zakończone + No comment provided by engineer. + + + Connections + Połączenia + No comment provided by engineer. + Contact allows Kontakt pozwala @@ -1433,6 +1977,11 @@ To jest twój jednorazowy link! Kontakt już istnieje No comment provided by engineer. + + Contact deleted! + Kontakt usunięty! + No comment provided by engineer. + Contact hidden: Kontakt ukryty: @@ -1443,9 +1992,9 @@ To jest twój jednorazowy link! Kontakt jest połączony notification - - Contact is not connected yet! - Kontakt nie jest jeszcze połączony! + + Contact is deleted. + Kontakt jest usunięty. No comment provided by engineer. @@ -1458,6 +2007,11 @@ To jest twój jednorazowy link! 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 @@ -1468,21 +2022,40 @@ To jest twój jednorazowy link! Kontakty mogą oznaczać wiadomości do usunięcia; będziesz mógł je zobaczyć. No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue Kontynuuj No comment provided by engineer. + + Conversation deleted! + Rozmowa usunięta! + No comment provided by engineer. + Copy Kopiuj - chat item action + 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 %@? @@ -1493,6 +2066,10 @@ To jest twój jednorazowy link! Utwórz No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Utwórz adres SimpleX @@ -1503,11 +2080,6 @@ To jest twój jednorazowy link! 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ć. - No comment provided by engineer. - Create file Utwórz plik @@ -1528,6 +2100,10 @@ To jest twój jednorazowy link! Utwórz link No comment provided by engineer. + + Create list + 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/). 💻 @@ -1553,6 +2129,11 @@ To jest twój jednorazowy link! Utwórz swój profil No comment provided by engineer. + + Created + Utworzono + No comment provided by engineer. + Created at Utworzony o @@ -1563,11 +2144,6 @@ To jest twój jednorazowy link! Utworzony o: %@ copied message info - - Created on %@ - Utworzony w dniu %@ - No comment provided by engineer. - Creating archive link Tworzenie linku archiwum @@ -1583,11 +2159,20 @@ To jest twój jednorazowy link! Aktualny Pin 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 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 %@. @@ -1598,11 +2183,25 @@ To jest twój jednorazowy link! Niestandardowy czas No comment provided by engineer. + + Customizable message shape. + 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 ID ID bazy danych @@ -1701,6 +2300,11 @@ To jest twój jednorazowy link! Baza danych zostanie zmigrowana po ponownym uruchomieniu aplikacji No comment provided by engineer. + + Debug delivery + Dostarczenie debugowania + No comment provided by engineer. + Decentralized Zdecentralizowane @@ -1714,18 +2318,19 @@ To jest twój jednorazowy link! Delete Usuń - chat item action + alert action +swipe action + + + Delete %lld messages of members? + Usunąć %lld wiadomości członków? + No comment provided by engineer. Delete %lld messages? Usunąć %lld wiadomości? No comment provided by engineer. - - Delete Contact - Usuń Kontakt - No comment provided by engineer. - Delete address Usuń adres @@ -1751,14 +2356,12 @@ To jest twój jednorazowy link! Usuń i powiadom kontakt No comment provided by engineer. - - Delete archive - Usuń archiwum + + Delete chat No comment provided by engineer. - - Delete chat archive? - Usunąć archiwum czatu? + + Delete chat messages from your device. No comment provided by engineer. @@ -1771,6 +2374,10 @@ To jest twój jednorazowy link! Usunąć profil czatu? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Usuń połączenie @@ -1781,11 +2388,9 @@ To jest twój jednorazowy link! Usuń kontakt No comment provided by engineer. - - Delete contact? -This cannot be undone! - Usunąć kontakt? -To nie może być cofnięte! + + Delete contact? + Usunąć kontakt? No comment provided by engineer. @@ -1848,6 +2453,10 @@ To nie może być cofnięte! Usunąć link? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Usunąć wiadomość członka? @@ -1861,7 +2470,7 @@ To nie może być cofnięte! Delete messages Usuń wiadomości - No comment provided by engineer. + alert button Delete messages after @@ -1878,9 +2487,8 @@ To nie może być cofnięte! Usunąć starą bazę danych? No comment provided by engineer. - - Delete pending connection - Usuń oczekujące połączenie + + Delete or moderate up to 200 messages. No comment provided by engineer. @@ -1898,11 +2506,30 @@ To nie może być cofnięte! Usuń kolejkę server test step + + Delete report + 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 + No comment provided by engineer. + + + Deleted + Usunięto + No comment provided by engineer. + Deleted at Usunięto o @@ -1913,6 +2540,15 @@ To nie może być cofnięte! Usunięto o: %@ copied message info + + Deletion errors + Błędy usuwania + No comment provided by engineer. + + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Dostarczenie @@ -1948,11 +2584,41 @@ To nie może być cofnięte! 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 %@. + No comment provided by engineer. + + + Destination server error: %@ + Błąd docelowego serwera: %@ + snd error text + + + 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 @@ -1983,8 +2649,12 @@ To nie może być cofnięte! Bezpośrednie wiadomości chat feature - - 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. Bezpośrednie wiadomości między członkami są zabronione w tej grupie. No comment provided by engineer. @@ -1998,11 +2668,24 @@ To nie może być cofnięte! Wyłącz blokadę SimpleX authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Wyłącz dla wszystkich No comment provided by engineer. + + Disabled + Wyłączony + No comment provided by engineer. + Disappearing message Znikająca wiadomość @@ -2018,8 +2701,8 @@ To nie może być cofnięte! Znikające wiadomości są zabronione na tym czacie. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Znikające wiadomości są zabronione w tej grupie. No comment provided by engineer. @@ -2053,11 +2736,21 @@ To nie może być cofnięte! Odkryj przez sieć lokalną 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 SimpleX for emergency calls. NIE używaj SimpleX do połączeń alarmowych. No comment provided by engineer. + + Do NOT use private routing. + NIE używaj prywatnego trasowania. + No comment provided by engineer. + Do it later Zrób to później @@ -2068,6 +2761,15 @@ To nie może być cofnięte! Nie wysyłaj historii do nowych członków. No comment provided by engineer. + + Do not use credentials with proxy. + Nie używaj danych logowania do proxy. + No comment provided by engineer. + + + Documents: + No comment provided by engineer. + Don't create address Nie twórz adresu @@ -2078,11 +2780,19 @@ To nie może być cofnięte! Nie włączaj No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Nie pokazuj ponownie No comment provided by engineer. + + Done + No comment provided by engineer. + Downgrade and open chat Obniż wersję i otwórz czat @@ -2091,7 +2801,13 @@ To nie może być cofnięte! Download Pobierz - chat item action + alert button +chat item action + + + Download errors + Błędy pobierania + No comment provided by engineer. Download failed @@ -2103,6 +2819,21 @@ To nie może być cofnięte! Pobierz plik server test step + + Download files + Pobierz pliki + alert action + + + Downloaded + Pobrane + No comment provided by engineer. + + + Downloaded files + Pobrane pliki + No comment provided by engineer. + Downloading archive Pobieranie archiwum @@ -2123,6 +2854,10 @@ To nie może być cofnięte! Czas trwania No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Edytuj @@ -2143,6 +2878,10 @@ To nie może być cofnięte! Włącz (zachowaj nadpisania) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock Włącz blokadę SimpleX @@ -2156,7 +2895,7 @@ To nie może być cofnięte! Enable automatic message deletion? Czy włączyć automatyczne usuwanie wiadomości? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2942,11 @@ To nie może być cofnięte! Włącz pin samodestrukcji set passcode view + + Enabled + Włączony + No comment provided by engineer. + Enabled for Włączony dla @@ -2278,6 +3022,10 @@ To nie może być cofnięte! Renegocjacja szyfrowania nie powiodła się. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Wprowadź Pin @@ -2343,26 +3091,34 @@ To nie może być cofnięte! Błąd przerwania zmiany adresu No comment provided by engineer. + + Error accepting conditions + alert title + 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 adding server + alert title + 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 @@ -2373,6 +3129,20 @@ To nie może być cofnięte! Błąd zmiany ustawienia No comment provided by engineer. + + Error changing to incognito! + Błąd zmiany na incognito! + No comment provided by engineer. + + + Error checking token status + 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 @@ -2388,6 +3158,10 @@ To nie może być cofnięte! Błąd tworzenia linku grupy No comment provided by engineer. + + Error creating list + alert title + Error creating member contact Błąd tworzenia kontaktu członka @@ -2403,6 +3177,10 @@ To nie może być cofnięte! Błąd tworzenia profilu! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Błąd odszyfrowania pliku @@ -2423,11 +3201,6 @@ To nie może być cofnięte! 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 @@ -2473,6 +3246,11 @@ To nie może być cofnięte! 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 @@ -2483,9 +3261,13 @@ To nie może być cofnięte! Błąd dołączenia do grupy No comment provided by engineer. - - Error loading %@ servers - Błąd ładowania %@ serwerów + + Error loading servers + alert title + + + Error migrating settings + Błąd migracji ustawień No comment provided by engineer. @@ -2496,16 +3278,34 @@ To nie może być cofnięte! Error receiving file Błąd odbioru pliku + alert title + + + 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 registering for notifications + alert title + Error removing member Błąd usuwania członka No comment provided by engineer. - - Error saving %@ servers - Błąd zapisu %@ serwerów + + Error reordering lists + alert title + + + Error resetting statistics + Błąd resetowania statystyk No comment provided by engineer. @@ -2513,6 +3313,10 @@ To nie może być cofnięte! Błąd zapisu serwerów ICE No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile Błąd zapisu profilu grupy @@ -2528,6 +3332,10 @@ To nie może być cofnięte! Błąd zapisu hasła do pęku kluczy No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Błąd zapisywania ustawień @@ -2573,16 +3381,25 @@ To nie może być cofnięte! Błąd zatrzymania czatu No comment provided by engineer. + + Error switching profile + Błąd zmiany profilu + No comment provided by engineer. + Error switching profile! Błąd przełączania profilu! - No comment provided by engineer. + alertTitle Error synchronizing connection Błąd synchronizacji połączenia No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link Błąd aktualizacji linku grupy @@ -2593,6 +3410,10 @@ To nie może być cofnięte! Błąd aktualizacji wiadomości No comment provided by engineer. + + Error updating server + alert title + Error updating settings Błąd aktualizacji ustawień @@ -2621,7 +3442,9 @@ To nie może być cofnięte! Error: %@ Błąd: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3456,15 @@ To nie może być cofnięte! Błąd: brak pliku bazy danych No comment provided by engineer. + + Errors + Błędy + No comment provided by engineer. + + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Nawet po wyłączeniu w rozmowie. @@ -2648,6 +3480,10 @@ To nie może być cofnięte! Rozszerz chat item action + + Expired + token status text + Export database Eksportuj bazę danych @@ -2658,6 +3494,11 @@ To nie może być cofnięte! Błąd eksportu: No comment provided by engineer. + + Export theme + Eksportuj motyw + No comment provided by engineer. + Exported database archive. Wyeksportowane archiwum bazy danych. @@ -2683,16 +3524,65 @@ To nie może być cofnięte! Szybko i bez czekania aż nadawca będzie online! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Szybsze dołączenie i bardziej niezawodne wiadomości. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Ulubione + swipe action + + + Favorites No comment provided by engineer. + + File error + Błąd pliku + file error alert title + + + File errors: +%@ + Błędy pliku: +%@ + alert message + + + File is blocked by server operator: +%@. + 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: %@ + file error text + + + File status + Status pliku + No comment provided by engineer. + + + File status: %@ + Status pliku: %@ + copied message info + File will be deleted from servers. Plik zostanie usunięty z serwerów. @@ -2713,6 +3603,11 @@ To nie może być cofnięte! Plik: %@ No comment provided by engineer. + + Files + Pliki + No comment provided by engineer. + Files & media Pliki i media @@ -2723,8 +3618,8 @@ To nie może być cofnięte! Pliki i media chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Pliki i media są zabronione w tej grupie. No comment provided by engineer. @@ -2793,21 +3688,64 @@ To nie może być cofnięte! Naprawa nie jest obsługiwana przez członka grupy No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + servers error + For console Dla konsoli 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. + No comment provided by engineer. + + + For me + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Przekaż dalej chat item action + + Forward %d message(s)? + Przekazać %d wiadomość(i)? + alert title + Forward and save messages Przesyłaj dalej i zapisuj wiadomości No comment provided by engineer. + + Forward messages + Przekaż wiadomości + alert action + + + Forward messages without files? + Przekazać wiadomości bez plików? + alert message + + + Forward up to 20 messages at once. + No comment provided by engineer. + Forwarded Przekazane dalej @@ -2818,6 +3756,40 @@ To nie może być cofnięte! 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Serwer przekazujący: %1$@ +Błąd serwera docelowego: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Serwer przekazujący: %1$@ +Błąd: %2$@ + snd error text + Found desktop Znaleziono komputer @@ -2838,11 +3810,6 @@ To nie może być cofnięte! 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. @@ -2863,6 +3830,20 @@ To nie może być cofnięte! GIF-y i naklejki No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + Dzień dobry! + message preview + + + Good morning! + Dzień dobry! + message preview + Group Grupa @@ -2918,41 +3899,6 @@ To nie może być cofnięte! 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. (24 hours) - Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny) - No comment provided by engineer. - - - Group members can send SimpleX links. - Członkowie grupy mogą wysyłać linki SimpleX. - 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. - No comment provided by engineer. - Group message: Wiadomość grupowa: @@ -2993,11 +3939,19 @@ To nie może być cofnięte! Grupa zostanie usunięta dla Ciebie - nie można tego cofnąć! No comment provided by engineer. + + Groups + No comment provided by engineer. + Help Pomoc No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Ukryte @@ -3048,10 +4002,17 @@ To nie może być cofnięte! Jak działa SimpleX No comment provided by engineer. + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy + No comment provided by engineer. + How it works - Jak to działa - No comment provided by engineer. + alert button How to @@ -3078,6 +4039,11 @@ To nie może być cofnięte! Serwery ICE (po jednym na linię) No comment provided by engineer. + + IP address + Adres IP + 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. @@ -3118,8 +4084,8 @@ To nie może być cofnięte! Natychmiast No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Odporność na spam i nadużycia No comment provided by engineer. @@ -3143,11 +4109,21 @@ To nie może być cofnięte! 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 delivery, reduced traffic usage. +More improvements are coming soon! + No comment provided by engineer. + Improved message delivery Ulepszona dostawa wiadomości @@ -3178,6 +4154,14 @@ To nie może być cofnięte! Dźwięki w rozmowie No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Incognito @@ -3248,6 +4232,11 @@ To nie może być cofnięte! 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! @@ -3255,16 +4244,36 @@ To nie może być cofnięte! No comment provided by engineer. - - Instantly - Natychmiastowo - No comment provided by engineer. - Interface Interfejs No comment provided by engineer. + + Interface colors + Kolory interfejsu + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code Nieprawidłowy kod QR @@ -3303,7 +4312,7 @@ To nie może być cofnięte! Invalid server address! Nieprawidłowy adres serwera! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4334,10 @@ To nie może być cofnięte! Zaproś członków No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Zaproś do grupy @@ -3340,8 +4353,8 @@ To nie może być cofnięte! Nieodwracalne usuwanie wiadomości jest na tym czacie zabronione. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione. No comment provided by engineer. @@ -3366,6 +4379,11 @@ To nie może być cofnięte! 3. 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 (%@). @@ -3384,7 +4402,7 @@ To nie może być cofnięte! Join Dołącz - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4444,11 @@ To jest twój link do grupy %@! Keep Zachowaj + alert action + + + Keep conversation + Zachowaj rozmowę No comment provided by engineer. @@ -3436,7 +4459,7 @@ To jest twój link do grupy %@! Keep unused invitation? Zachować nieużyte zaproszenie? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4494,14 @@ To jest twój link do grupy %@! Leave Opuść + swipe action + + + Leave chat + No comment provided by engineer. + + + Leave chat? No comment provided by engineer. @@ -3513,6 +4544,18 @@ To jest twój link do grupy %@! Połączone komputery No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! Wiadomość na żywo! @@ -3523,11 +4566,6 @@ To jest twój link do grupy %@! Wiadomości na żywo No comment provided by engineer. - - Local - Lokalnie - No comment provided by engineer. - Local name Nazwa lokalna @@ -3548,11 +4586,6 @@ To jest twój link do grupy %@! 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 @@ -3563,21 +4596,11 @@ To jest twój link do grupy %@! 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 @@ -3603,11 +4626,34 @@ To jest twój link do grupy %@! Maksymalnie 30 sekund, odbierane natychmiast. No comment provided by engineer. + + Media & file servers + Serwery mediów i plików + No comment provided by engineer. + + + Medium + Średni + blur media + Member Członek No comment provided by engineer. + + Member inactive + Członek nieaktywny + item status text + + + Member reports + chat feature + + + 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. Rola członka grupy zostanie zmieniona na "%@". Wszyscy członkowie grupy zostaną powiadomieni. @@ -3618,11 +4664,63 @@ To jest twój link do grupy %@! Rola członka zostanie zmieniona na "%@". Członek otrzyma nowe zaproszenie. 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! 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 report messsages to moderators. + 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 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 voice messages. + Członkowie grupy mogą wysyłać wiadomości głosowe. + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + Menu + No comment provided by engineer. + Message delivery error Błąd dostarczenia wiadomości @@ -3633,11 +4731,31 @@ To jest twój link do grupy %@! Potwierdzenia dostarczenia wiadomości! No comment provided by engineer. + + Message delivery warning + Ostrzeżenie dostarczenia wiadomości + item status text + Message draft Wersja robocza wiadomości No comment provided by engineer. + + Message forwarded + Wiadomość przekazana + item status text + + + Message may be delivered later if member becomes active. + Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny. + item status description + + + Message queue info + Informacje kolejki wiadomości + No comment provided by engineer. + Message reactions Reakcje wiadomości @@ -3648,16 +4766,41 @@ To jest twój link do grupy %@! Reakcje wiadomości są zabronione na tym czacie. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Reakcje wiadomości są zabronione w tej grupie. 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 + No comment provided by engineer. + + + Message status: %@ + Status wiadomości: %@ + copied message info + Message text Tekst wiadomości @@ -3683,6 +4826,25 @@ To jest twój link do grupy %@! Wiadomości od %@ zostaną pokazane! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + Otrzymane wiadomości + No comment provided by engineer. + + + Messages sent + Wysłane wiadomości + No comment provided by engineer. + + + Messages were deleted after you selected them. + Wiadomości zostały usunięte po wybraniu ich. + alert message + 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. @@ -3748,9 +4910,9 @@ To jest twój link do grupy %@! Migracja została zakończona No comment provided by engineer. - - Migrations: %@ - Migracje: %@ + + Migrations: + Migracje: No comment provided by engineer. @@ -3768,6 +4930,10 @@ To jest twój link do grupy %@! Moderowany o: %@ copied message info + + More + swipe action + More improvements are coming soon! Więcej ulepszeń już wkrótce! @@ -3778,16 +4944,15 @@ To jest twój link do grupy %@! Bardziej niezawodne połączenia sieciowe. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Najprawdopodobniej to połączenie jest usunięte. item status description - - 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 @@ -3796,7 +4961,11 @@ To jest twój link do grupy %@! Mute Wycisz - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3806,7 +4975,7 @@ To jest twój link do grupy %@! Name Nazwa - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +4987,24 @@ To jest twój link do grupy %@! Połączenie z siecią No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej. + snd error text + Network management Zarządzenie sieciowe No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Ustawienia sieci @@ -3833,16 +5015,35 @@ To jest twój link do grupy %@! Status sieci No comment provided by engineer. + + New + token status text + New Passcode Nowy Pin 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. + No comment provided by engineer. + New chat Nowy czat No comment provided by engineer. + + New chat experience 🎉 + Nowe możliwości czatu 🎉 + No comment provided by engineer. + New contact request Nowa prośba o kontakt @@ -3853,11 +5054,6 @@ To jest twój link do grupy %@! Nowy kontakt: notification - - New database archive - Nowe archiwum bazy danych - No comment provided by engineer. - New desktop app! Nowa aplikacja desktopowa! @@ -3868,11 +5064,20 @@ To jest twój link do grupy %@! Nowa wyświetlana nazwa No comment provided by engineer. + + New events + notification + 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 @@ -3888,6 +5093,10 @@ To jest twój link do grupy %@! Nowe hasło… No comment provided by engineer. + + New server + No comment provided by engineer. + No Nie @@ -3898,6 +5107,18 @@ To jest twój link do grupy %@! Brak hasła aplikacji Authentication unavailable + + 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 Nie wybrano kontaktów @@ -3918,6 +5139,11 @@ To jest twój link do grupy %@! Brak tokenu urządzenia! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora. + item status description + No filtered chats Brak filtrowanych czatów @@ -3933,26 +5159,101 @@ To jest twój link do grupy %@! Brak historii No comment provided by engineer. + + No info, try to reload + Brak informacji, spróbuj przeładować + No comment provided by engineer. + + + No media & file servers. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + servers error + 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 No comment provided by engineer. + + 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. + servers error + + + No token! + alert title + + + No unread chats + 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. + + Notes + No comment provided by engineer. + + + Nothing selected + Nic nie jest zaznaczone + No comment provided by engineer. + + + Nothing to forward! + Nic do przekazania! + alert title + Notifications Powiadomienia @@ -3963,6 +5264,18 @@ To jest twój link do grupy %@! Powiadomienia są wyłączone! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5293,35 @@ To jest twój link do grupy %@! Off Wyłączony - No comment provided by engineer. + blur media Ok Ok - No comment provided by engineer. + alert button Old database Stara baza danych No comment provided by engineer. - - Old database archive - Stare archiwum bazy danych - No comment provided by engineer. - 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. +Requires compatible VPN. + Hosty onion będą wymagane do połączenia. +Wymaga 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. +Requires compatible VPN. + Hosty onion będą używane, gdy będą dostępne. +Wymaga włączenia VPN. No comment provided by engineer. @@ -4017,11 +5329,20 @@ To jest twój link do grupy %@! 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**. + + Only chat owners can change preferences. + No comment provided by engineer. + + + 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. @@ -4037,6 +5358,14 @@ To jest twój link do grupy %@! Tylko właściciele grup mogą włączyć wiadomości głosowe. 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. Tylko Ty możesz dodawać reakcje wiadomości. @@ -4090,13 +5419,17 @@ To jest twój link do grupy %@! Open Otwórz - No comment provided by engineer. + alert action Open Settings Otwórz Ustawienia No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Otwórz czat @@ -4107,31 +5440,41 @@ To jest twój link do grupy %@! Otwórz konsolę czatu authentication reason + + Open conditions + No comment provided by engineer. + Open group Grupa otwarta No comment provided by engineer. + + Open link? + alert title + Open migration to another device Otwórz migrację na innym urządzeniu authentication reason - - Open user profiles - Otwórz profile użytkownika - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Otwarto źródłowy protokół i kod - każdy może uruchomić serwery. - No comment provided by engineer. - Opening app… Otwieranie aplikacji… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + + + Or import archive file + No comment provided by engineer. + Or paste archive link Lub wklej link archiwum @@ -4152,11 +5495,26 @@ To jest twój link do grupy %@! Lub pokaż ten kod No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + + + Organize chats into lists + No comment provided by engineer. + Other Inne No comment provided by engineer. + + Other file errors: +%@ + Inne błędy pliku: +%@ + alert message + PING count Liczba PINGÓW @@ -4192,6 +5550,11 @@ To jest twój link do grupy %@! Pin ustawiony! No comment provided by engineer. + + Password + Hasło + No comment provided by engineer. + Password to show Hasło do wyświetlenia @@ -4222,13 +5585,13 @@ To jest twój link do grupy %@! Wklej link, który otrzymałeś 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 + + Periodic Okresowo No comment provided by engineer. @@ -4242,11 +5605,28 @@ To jest twój link do grupy %@! Połączenia obraz-w-obrazie 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. +Please 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. +Proszę 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. @@ -4314,11 +5694,28 @@ Błąd: %@ Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można go zmienić. No comment provided by engineer. + + 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. + token info + Polish interface Polski interfejs No comment provided by engineer. + + Port + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy @@ -4329,41 +5726,81 @@ Błąd: %@ 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. + + Preset servers + 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 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 Redefinicja prywatności No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + No comment provided by engineer. + Private filenames Prywatne nazwy plików No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + + + Private message routing + Trasowanie prywatnych wiadomości + No comment provided by engineer. + + + Private message routing 🚀 + Trasowanie prywatnych wiadomości🚀 + No comment provided by engineer. + Private notes Prywatne notatki name of notes to self + + 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 @@ -4379,25 +5816,20 @@ Błąd: %@ Zdjęcia profilowe No comment provided by engineer. - - Profile name - Nazwa profilu - No comment provided by engineer. - - - Profile name: - Nazwa profilu: - No comment provided by engineer. - Profile password Hasło profilu No comment provided by engineer. + + Profile theme + Motyw profilu + No comment provided by engineer. + Profile update will be sent to your contacts. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,6 +5851,10 @@ Błąd: %@ Zabroń reakcje wiadomości. No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. Zabroń wysyłania linków SimpleX. @@ -4444,11 +5880,23 @@ Błąd: %@ Zabroń wysyłania wiadomości głosowych. No comment provided by engineer. + + Protect IP address + Chroń adres IP + No comment provided by engineer. + Protect app screen Chroń ekran aplikacji No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Chroni Twój adres IP przed przekaźnikami wiadomości wybranych przez Twoje kontakty. +Włącz w ustawianiach *Sieć i serwery* . + No comment provided by engineer. + Protect your chat profiles with a password! Chroń swoje profile czatu hasłem! @@ -4464,6 +5912,21 @@ Błąd: %@ 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 @@ -4484,6 +5947,11 @@ Błąd: %@ Oceń aplikację No comment provided by engineer. + + Reachable chat toolbar + Osiągalny pasek narzędzi czatu + No comment provided by engineer. + React… Reaguj… @@ -4492,33 +5960,28 @@ Błąd: %@ Read Czytaj - No comment provided by engineer. + swipe action 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). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Przeczytaj więcej na naszym repozytorium GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Przeczytaj więcej na naszym [repozytorium GitHub](https://github.com/simplex-chat/simplex-chat#readme). @@ -4529,6 +5992,11 @@ Błąd: %@ Potwierdzenia są wyłączone No comment provided by engineer. + + Receive errors + Błędy otrzymania + No comment provided by engineer. + Received at Otrzymane o @@ -4549,16 +6017,26 @@ Błąd: %@ Otrzymano wiadomość message info title + + 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. No comment provided by engineer. - - Receiving concurrency - Konkurencyjne odbieranie - No comment provided by engineer. - Receiving file will be stopped. Odbieranie pliku zostanie przerwane. @@ -4584,11 +6062,36 @@ Błąd: %@ 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? @@ -4609,10 +6112,23 @@ Błąd: %@ Zmniejszone zużycie baterii No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject Odrzuć - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4639,6 +6155,16 @@ Błąd: %@ 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 @@ -4699,6 +6225,46 @@ Błąd: %@ Odpowiedz chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required Wymagane @@ -4709,16 +6275,41 @@ Błąd: %@ 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 @@ -4759,9 +6350,8 @@ Błąd: %@ Ujawnij chat item action - - Revert - Przywrócić + + Review conditions No comment provided by engineer. @@ -4789,9 +6379,19 @@ Błąd: %@ Uruchom czat No comment provided by engineer. - - SMP servers - Serwery SMP + + SMP server + Serwer SMP + No comment provided by engineer. + + + SOCKS proxy + Proxy SOCKS + No comment provided by engineer. + + + Safely receive files + Bezpiecznie otrzymuj pliki No comment provided by engineer. @@ -4802,43 +6402,43 @@ Błąd: %@ Save Zapisz - chat item action + alert button +chat item action 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. + alert button 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 No comment provided by engineer. + + Save list + No comment provided by engineer. + Save passphrase and open chat Zapisz hasło i otwórz czat @@ -4852,7 +6452,7 @@ Błąd: %@ Save preferences? Zapisać preferencje? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6467,18 @@ Błąd: %@ Save servers? Zapisać serwery? - No comment provided by engineer. - - - Save settings? - Zapisać ustawienia? - No comment provided by engineer. + alert title Save welcome message? Zapisać wiadomość powitalną? No comment provided by engineer. + + Save your profile? + Zapisać Twój profil? + alert title + Saved Zapisane @@ -4899,6 +6499,21 @@ Błąd: %@ Zachowano wiadomość message info title + + 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 QR code Zeskanuj kod QR @@ -4939,11 +6554,21 @@ Błąd: %@ Wyszukaj lub wklej link SimpleX No comment provided by engineer. + + Secondary + Drugorzędny + No comment provided by engineer. + Secure queue Bezpieczna kolejka server test step + + Secured + Zabezpieczone + No comment provided by engineer. + Security assessment Ocena bezpieczeństwa @@ -4957,6 +6582,21 @@ Błąd: %@ Select Wybierz + chat item action + + + 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. @@ -4994,11 +6634,6 @@ Błąd: %@ Wyślij potwierdzenia dostawy do 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ć @@ -5009,6 +6644,11 @@ Błąd: %@ 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 @@ -5019,14 +6659,28 @@ Błąd: %@ Wyślij wiadomość na żywo No comment provided by engineer. + + Send message to enable calls. + Wyślij wiadomość aby włączyć połączenia. + No comment provided by engineer. + + + 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 notifications: - Wyślij powiadomienia: + + Send private reports No comment provided by engineer. @@ -5052,7 +6706,7 @@ Błąd: %@ Sender cancelled file transfer. Nadawca anulował transfer pliku. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6763,11 @@ Błąd: %@ Wysłano o: %@ copied message info + + Sent directly + Wysłano bezpośrednio + No comment provided by engineer. + Sent file event Wyślij zdarzenie pliku @@ -5119,11 +6778,67 @@ Błąd: %@ Wyślij wiadomość message info title + + 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 added to operator %@. + alert message + + + Server address + Adres serwera + No comment provided by engineer. + + + Server address is incompatible with network settings. + Adres serwera jest niekompatybilny z ustawieniami sieciowymi. + srv error text. + + + Server address is incompatible with network settings: %@. + Adres serwera jest niekompatybilny z ustawieniami sieci: %@. + No comment provided by engineer. + + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło @@ -5139,11 +6854,36 @@ Błąd: %@ Test serwera nie powiódł się! No comment provided by engineer. + + Server type + Typ serwera + No comment provided by engineer. + + + Server version is incompatible with network settings. + Wersja serwera jest niekompatybilna z ustawieniami sieciowymi. + srv error text + + + 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 @@ -5154,11 +6894,20 @@ Błąd: %@ Ustaw 1 dzień No comment provided by engineer. + + Set chat name… + 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 @@ -5169,6 +6918,10 @@ Błąd: %@ Ustaw go zamiast uwierzytelniania systemowego. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Ustaw pin @@ -5199,6 +6952,11 @@ Błąd: %@ Ustawienia No comment provided by engineer. + + Settings were changed. + Ustawienia zostały zmienione. + alert message + Shape profile images Kształtuj obrazy profilowe @@ -5207,21 +6965,39 @@ Błąd: %@ Share Udostępnij - chat item action + alert action +chat item action Share 1-time link Udostępnij 1-razowy link No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Udostępnij adres No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Udostępnić adres kontaktom? + alert title + + + Share from other apps. + Udostępnij z innych aplikacji. No comment provided by engineer. @@ -5229,16 +7005,30 @@ Błąd: %@ Udostępnij link No comment provided by engineer. + + 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. + + Short link + No comment provided by engineer. + Show QR code Pokaż kod QR @@ -5259,21 +7049,45 @@ Błąd: %@ 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 → on messages sent via private routing. + Pokaż → na wiadomościach wysłanych przez prywatne trasowanie. + No comment provided by engineer. + Show: Pokaż: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address Adres SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Bezpieczeństwo SimpleX Chat zostało zaudytowane przez Trail of Bits. @@ -5304,6 +7118,18 @@ Błąd: %@ Adres SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + + + SimpleX channel link + simplex link type + SimpleX contact address Adres kontaktowy SimpleX @@ -5324,8 +7150,8 @@ Błąd: %@ Linki SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. Linki SimpleX są zablokowane na tej grupie. No comment provided by engineer. @@ -5339,11 +7165,20 @@ Błąd: %@ Zaproszenie jednorazowe SimpleX simplex link type + + SimpleX protocols reviewed by Trail of Bits. + 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ń @@ -5359,16 +7194,46 @@ Błąd: %@ Małe grupy (maks. 20) No comment provided by engineer. + + Soft + Łagodny + blur media + + + 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + alert message + Somebody Ktoś notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. Kwadrat, okrąg lub cokolwiek pomiędzy. @@ -5389,6 +7254,16 @@ Błąd: %@ Rozpocznij migrację No comment provided by engineer. + + Starting from %@. + Zaczynanie od %@. + No comment provided by engineer. + + + Statistics + Statystyki + No comment provided by engineer. + Stop Zatrzymaj @@ -5404,11 +7279,6 @@ Błąd: %@ Zatrzymaj czat No comment provided by engineer. - - Stop chat to enable database actions - Zatrzymaj czat, aby umożliwić działania na bazie danych - 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. @@ -5437,28 +7307,60 @@ Błąd: %@ Stop sharing Przestań udostępniać - No comment provided by engineer. + alert action Stop sharing address? Przestać udostępniać adres? - No comment provided by engineer. + alert title Stopping chat Zatrzymywanie czatu No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + Silne + blur media + 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 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 System @@ -5469,11 +7371,20 @@ Błąd: %@ Uwierzytelnianie systemu No comment provided by engineer. + + TCP connection + Połączenie TCP + No comment provided by engineer. + TCP connection timeout Limit czasu połączenia TCP No comment provided by engineer. + + TCP port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5489,11 +7400,20 @@ Błąd: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Ogon + No comment provided by engineer. + Take picture Zrób zdjęcie No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Naciśnij przycisk @@ -5529,16 +7449,20 @@ Błąd: %@ Dotknij, aby zeskanować No comment provided by engineer. - - Tap to start a new chat - Dotknij, aby rozpocząć nowy czat - No comment provided by engineer. + + Temporary file error + Tymczasowy błąd pliku + file error alert title Test failed at step %@. Test nie powiódł się na etapie %@. server test failure + + Test notifications + No comment provided by engineer. + Test server Przetestuj serwer @@ -5552,7 +7476,7 @@ Błąd: %@ Tests failed! Testy nie powiodły się! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7493,6 @@ Błąd: %@ 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 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. @@ -5586,6 +7505,15 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom 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 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). + 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. @@ -5596,6 +7524,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Kod, który zeskanowałeś nie jest kodem QR linku SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Zaakceptowane przez Ciebie połączenie zostanie anulowane! @@ -5616,6 +7548,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Szyfrowanie działa, a nowe uzgodnienie szyfrowania nie jest wymagane. Może to spowodować błędy w połączeniu! No comment provided by engineer. + + The future of messaging + Następna generacja prywatnych wiadomości + No comment provided by engineer. + The hash of the previous message is different. Hash poprzedniej wiadomości jest inny. @@ -5631,9 +7568,14 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom 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. @@ -5641,9 +7583,12 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Stara baza danych nie została usunięta podczas migracji, można ją usunąć. No comment provided by engineer. - - The profile is only shared with your contacts. - Profil jest udostępniany tylko Twoim kontaktom. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! No comment provided by engineer. @@ -5661,14 +7606,27 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Serwery dla nowych połączeń bieżącego profilu czatu **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Tekst, który wkleiłeś nie jest linkiem SimpleX. No comment provided by engineer. - - Theme - Motyw + + 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. + + + Themes + Motywy + No comment provided by engineer. + + + These conditions will also apply for: **%@**. No comment provided by engineer. @@ -5691,6 +7649,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom 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. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + 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. @@ -5736,11 +7698,29 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom To jest twój jednorazowy link! 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. + 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 message was deleted or not received yet. + 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: @@ -5761,9 +7741,8 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom 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. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -5771,6 +7750,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Aby chronić strefę czasową, pliki obrazów/głosów używają UTC. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,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. 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 receive + 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. @@ -5788,26 +7791,58 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Aby ujawnić Twój ukryty profil, wprowadź pełne hasło w pole wyszukiwania na stronie **Twoich profili czatu**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Aby obsługiwać natychmiastowe powiadomienia push, należy zmigrować bazę danych czatu. 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. 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. + + Token status: %@. + token status + + + 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: %@). @@ -5863,10 +7898,9 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Odblokować członka? No comment provided by engineer. - - Unexpected error: %@ - Nieoczekiwany błąd: %@ - item status description + + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5876,7 +7910,7 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania. Unfav. Nie ulub. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +7947,11 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Nieznany błąd No comment provided by engineer. + + Unknown servers! + Nieznane serwery! + alert title + 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. @@ -5948,11 +7987,15 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Unmute Wyłącz wyciszenie - No comment provided by engineer. + notification label action Unread Nieprzeczytane + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5965,11 +8008,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Aktualizuj No comment provided by engineer. - - Update .onion hosts setting? - Zaktualizować ustawienie hostów .onion? - No comment provided by engineer. - Update database passphrase Aktualizuj hasło do bazy danych @@ -5980,9 +8018,13 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Zaktualizować ustawienia sieci? No comment provided by engineer. - - Update transport isolation mode? - Zaktualizować tryb izolacji transportu? + + Update settings? + Zaktualizować ustawienia? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5990,16 +8032,16 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc 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. - No comment provided by engineer. - Upgrade and open chat Zaktualizuj i otwórz czat No comment provided by engineer. + + Upload errors + Błędy przesłania + No comment provided by engineer. + Upload failed Wgrywanie nie udane @@ -6010,21 +8052,48 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Prześlij plik server test step + + 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 %@ + No comment provided by engineer. + Use .onion hosts Użyj hostów .onion No comment provided by engineer. + + Use SOCKS proxy + Użyj proxy SOCKS + No comment provided by engineer. + Use SimpleX Chat servers? Użyć serwerów SimpleX Chat? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Użyj czatu @@ -6035,6 +8104,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Użyj obecnego profilu 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 Użyj dla nowych połączeń @@ -6060,24 +8137,51 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc 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 No comment provided by engineer. + + Use servers + No comment provided by engineer. + + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. Używaj aplikacji podczas połączenia. No comment provided by engineer. - - User profile - Profil użytkownika + + Use the app with one hand. + Korzystaj z aplikacji jedną ręką. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Używanie hostów .onion wymaga kompatybilnego dostawcy VPN. + + Use web port + 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. @@ -6150,11 +8254,19 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Filmy i pliki do 1gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Pokaż kod bezpieczeństwa No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Widoczna historia @@ -6170,8 +8282,8 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wiadomości głosowe są zabronione na tym czacie. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Wiadomości głosowe są zabronione w tej grupie. No comment provided by engineer. @@ -6210,6 +8322,16 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc 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. + 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 @@ -6255,9 +8377,8 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Podczas łączenia połączeń audio i wideo. 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 more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -6295,24 +8416,39 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc 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. + No comment provided by engineer. + + + 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: %@. + alert message + Wrong database passphrase Nieprawidłowe hasło bazy danych No comment provided by engineer. + + 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. + snd 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. + file error text + Wrong passphrase! Nieprawidłowe hasło! No comment provided by engineer. - - XFTP servers - Serwery XFTP - No comment provided by engineer. - - - You - Ty + + XFTP server + Serwer XFTP No comment provided by engineer. @@ -6340,6 +8476,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Jesteś już połączony z %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Już się łączysz z %@. @@ -6387,11 +8527,25 @@ Powtórzyć prośbę dołączenia? 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 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 change it in Appearance settings. + Możesz to zmienić w ustawieniach wyglądu. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + You can create it later Możesz go utworzyć później @@ -6422,11 +8576,20 @@ Powtórzyć prośbę dołączenia? Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Możesz teraz wysyłać wiadomości do %@ notification body + + 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 connection name, to remember who the link was shared with. + 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. @@ -6442,16 +8605,16 @@ Powtórzyć prośbę dołączenia? 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ć. - 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 No comment provided by engineer. + + 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. @@ -6465,23 +8628,23 @@ Powtórzyć prośbę dołączenia? You can view invitation link again in connection details. Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia. - No comment provided by engineer. + alert message You can't send messages! Nie możesz wysyłać wiadomości! 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. - No comment provided by engineer. - You could not be verified; please try again. Nie można zweryfikować użytkownika; proszę spróbować ponownie. No comment provided by engineer. + + You decide who can connect. + Ty decydujesz, kto może się połączyć. + No comment provided by engineer. + You have already requested connection via this address! Już prosiłeś o połączenie na ten adres! @@ -6494,11 +8657,6 @@ Repeat connection request? Powtó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. @@ -6519,11 +8677,26 @@ Powtórzyć prośbę połączenia? Dołączyłeś do tej grupy. Łączenie z zapraszającym członkiem grupy. 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ć. @@ -6539,6 +8712,10 @@ Powtórzyć prośbę połączenia? Wysłałeś zaproszenie do grupy No comment provided by engineer. + + You should receive notifications. + token info + 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! @@ -6574,6 +8751,10 @@ Powtórzyć prośbę połączenia? Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Przestaniesz otrzymywać wiadomości od tej grupy. Historia czatu zostanie zachowana. @@ -6594,31 +8775,16 @@ Powtórzyć prośbę połączenia? 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 ICE servers Twoje serwery ICE No comment provided by engineer. - - Your SMP servers - Twoje serwery SMP - No comment provided by engineer. - Your SimpleX address Twój adres SimpleX No comment provided by engineer. - - Your XFTP servers - Twoje serwery XFTP - No comment provided by engineer. - Your calls Twoje połączenia @@ -6634,16 +8800,19 @@ Powtórzyć prośbę połączenia? Baza danych czatu nie jest szyfrowana - ustaw hasło, aby ją zaszyfrować. No comment provided by engineer. + + Your chat preferences + Twoje preferencje czatu + alert title + Your chat profiles Twoje profile czatu No comment provided by engineer. - - 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). - 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). + + 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. @@ -6661,6 +8830,11 @@ Możesz anulować to połączenie i usunąć kontakt (i spróbować później z Twoje kontakty pozostaną połączone. No comment provided by engineer. + + 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ą. @@ -6691,33 +8865,36 @@ Możesz anulować to połączenie i usunąć kontakt (i spróbować później z Twój profil **%@** zostanie udostępniony. No comment provided by engineer. - - 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. + + 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. - - 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. + + 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. No comment provided by engineer. + + 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. + alert message + Your random profile Twój losowy profil No comment provided by engineer. - - Your server - Twój serwer - No comment provided by engineer. - Your server address Twój adres serwera No comment provided by engineer. + + Your servers + Twoje serwery + No comment provided by engineer. + Your settings Twoje ustawienia @@ -6758,6 +8935,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. zaakceptowane połączenie call status + + accepted invitation + chat list item title + admin administrator @@ -6793,6 +8974,15 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. i %lld innych wydarzeń No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + próby + No comment provided by engineer. + audio call (not e2e encrypted) połączenie audio (nie szyfrowane e2e) @@ -6826,13 +9016,19 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. blocked by admin zablokowany przez admina - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold pogrubiona No comment provided by engineer. + + call + zadzwoń + No comment provided by engineer. + call error błąd połączenia @@ -6936,7 +9132,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. connecting… łączenie… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +9179,16 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. dni time unit + + decryption errors + błąd odszyfrowywania + No comment provided by engineer. + default (%@) domyślne (%@) - pref value + delete after time +pref value default (no) @@ -7033,6 +9235,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. zduplikowana wiadomość integrity error chat item + + duplicates + duplikaty + No comment provided by engineer. + e2e encrypted zaszyfrowany e2e @@ -7108,9 +9315,9 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. błąd No comment provided by engineer. - - event happened - nowe wydarzenie + + expired + wygasły No comment provided by engineer. @@ -7143,6 +9350,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. 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. + + inactive + nieaktywny + No comment provided by engineer. + incognito via contact address link incognito poprzez link adresu kontaktowego @@ -7183,6 +9395,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. zaproszenie do grupy %@ group name + + invite + zaproś + No comment provided by engineer. + invited zaproszony @@ -7238,6 +9455,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. połączony rcv group event chat item + + message + wiadomość + No comment provided by engineer. + message received wiadomość otrzymana @@ -7263,6 +9485,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. moderowany przez %@ marked deleted chat item preview text + + moderator + member role + months miesiące @@ -7271,7 +9497,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. never nigdy - No comment provided by engineer. + delete after time new message @@ -7302,8 +9528,8 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. off wyłączony enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9546,16 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. włączone group pref value + + other + inne + No comment provided by engineer. + + + other errors + inne błędy + No comment provided by engineer. + owner właściciel @@ -7335,6 +9571,14 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. peer-to-peer No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption kwantowo odporne szyfrowanie e2e @@ -7350,6 +9594,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. otrzymano potwierdzenie… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call odrzucone połączenie @@ -7380,6 +9628,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. usunął cię rcv group event chat item + + requested to connect + chat list item title + saved zapisane @@ -7390,6 +9642,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. zapisane od %@ No comment provided by engineer. + + search + szukaj + No comment provided by engineer. + sec sek @@ -7415,6 +9672,15 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. wyślij wiadomość bezpośrednią No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + Informacje kolejki serwera: %1$@ + +ostatnia otrzymana wiadomość: %2$@ + queue info + set new contact address ustaw nowy adres kontaktu @@ -7455,11 +9721,21 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. nieznany connection info + + unknown servers + nieznane przekaźniki + No comment provided by engineer. + unknown status nieznany status No comment provided by engineer. + + unprotected + niezabezpieczony + No comment provided by engineer. + updated group profile zaktualizowano profil grupy @@ -7500,6 +9776,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. przez przekaźnik No comment provided by engineer. + + video + wideo + No comment provided by engineer. + video call (not e2e encrypted) połączenie wideo (bez szyfrowania e2e) @@ -7525,6 +9806,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. tygodnie time unit + + when IP hidden + gdy IP ukryty + No comment provided by engineer. + yes tak @@ -7609,7 +9895,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.
- +
@@ -7646,7 +9932,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.
- +
@@ -7666,4 +9952,245 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.
+ +
+ +
+ + + %d new events + notification body + + + From %d chat(s) + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Wszelkie prawa zastrzeżone. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 b4fa69449f..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 @@ -187,23 +187,18 @@ ) No comment provided by engineer.
- - **Add new contact**: to create your one-time QR Code or link for your contact. - **Adicionar novo contato**: para criar seu QR Code ou link único para seu contato. - No comment provided by engineer. - **Create link / QR code** for your contact to use. **Crie um link / QR code** para seu contato usar. 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. + + **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. **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. 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). + + **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. **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). No comment provided by engineer. @@ -217,8 +212,8 @@ **Observação**: NÃO será possível recuperar ou alterar a frase secreta se você a perder. 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. + + **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**: 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. No comment provided by engineer. @@ -374,9 +369,9 @@ Adicione servidores escaneando o QR code. No comment provided by engineer. - - Add server… - Adicionar servidor… + + Add server + Adicionar servidor No comment provided by engineer. @@ -1209,8 +1204,8 @@ Mensagens diretas chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Mensagens diretas entre membros são proibidas neste grupo. No comment provided by engineer. @@ -1229,8 +1224,8 @@ Mensagens temporárias são proibidas nesse chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Mensagens que temporárias são proibidas neste grupo. No comment provided by engineer. @@ -1643,18 +1638,18 @@ Os membros do grupo podem excluir mensagens enviadas de forma irreversível. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Os membros do grupo podem enviar DMs. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Os membros do grupo podem enviar mensagens que desaparecem. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Os membros do grupo podem enviar mensagens de voz. No comment provided by engineer. @@ -1761,8 +1756,8 @@ A imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Imune a spam e abuso No comment provided by engineer. @@ -1878,8 +1873,8 @@ A exclusão irreversível de mensagens é proibida neste chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. A exclusão irreversível de mensagens é proibida neste grupo. No comment provided by engineer. @@ -2209,8 +2204,8 @@ We will be adding server redundancy to prevent lost messages. Hosts Onion não serão usados. 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. @@ -2267,8 +2262,8 @@ We will be adding server redundancy to prevent lost messages. Abrir console de chat authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. Protocolo de código aberto – qualquer um pode executar os servidores. No comment provided by engineer. @@ -2306,8 +2301,8 @@ We will be adding server redundancy to prevent lost messages. Cole o link que você recebeu na caixa abaixo para conectar com o seu contato. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. Pessoas podem se conectar com você somente via links compartilhados. No comment provided by engineer. @@ -2961,8 +2956,8 @@ We will be adding server redundancy to prevent lost messages. Thank you for installing SimpleX Chat! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. A 1ª plataforma sem nenhum identificador de usuário – privada por design. No comment provided by engineer. @@ -2998,8 +2993,8 @@ We will be adding server redundancy to prevent lost messages. The microphone does not work when the app is in the background. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging A próxima geração de mensageiros privados No comment provided by engineer. @@ -3007,8 +3002,8 @@ We will be adding server redundancy to prevent lost messages. 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. O perfil é compartilhado apenas com seus contatos. No comment provided by engineer. @@ -3071,8 +3066,8 @@ We will be adding server redundancy to prevent lost messages. To prevent the call interruption, enable Do Not Disturb mode. 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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3274,8 +3269,8 @@ Para se conectar, peça ao seu contato para criar outro link de conexão e verif Mensagens de voz são proibidas neste chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Mensagens de voz são proibidas neste grupo. No comment provided by engineer. @@ -3402,10 +3397,6 @@ Para se conectar, peça ao seu contato para criar outro link de conexão e verif Você pode usar markdown para formatar mensagens: 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. Você não pôde ser verificado; por favor, tente novamente. @@ -5234,6 +5225,426 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi %1$@ em %2$@: copied message info, <sender> at <time> + + Allow your contacts to irreversibly delete sent messages. (24 hours) + Permitir que seus contatos deletem mensagens enviadas de maneira irreversível. (24 horas) + + + %@ downloaded + baixado + + + %@ uploaded + transferido + + + A new random profile will be shared. + Um novo perfil aleatório será compartilhado. + + + Camera not available + Câmera indisponível + + + Admins can block a member for all. + Administradores podem bloquear um membro para todos. + + + Allow to irreversibly delete sent messages. (24 hours) + Permitir que mensagens enviadas sejam deletadas de maneira irreversível. (24 horas) + + + Apply + Aplicar + + + Accent + Esquema + + + Accept connection request? + Aceitar solicitação de conexão? + + + Active connections + Conexões ativas + + + Add contact + Adicionar contato + + + Additional accent + Esquema adicional + + + All new messages from %@ will be hidden! + Todas as novas mensagens de %@ serão ocultas! + + + All profiles + Todos perfis + + + Allow calls? + Permitir chamadas? + + + Archive contacts to chat later. + Arquivar contatos para conversar depois. + + + Blur media + Censurar mídia + + + Calls prohibited! + Chamadas proibidas! + + + Can't call contact + Não foi possível ligar para o contato + + + %lld messages marked deleted + mensagens deletadas + + + 0 sec + 0 seg + + + %lld messages blocked + mensagens bloqueadas + + + %lld messages blocked by admin + mensagens bloqueadas pelo administrador + + + **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **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. + + + - more stable message delivery. +- a bit better groups. +- and more! + - entrega de mensagens mais estável. +- grupos melhorados. +- e muito mais! + + + All messages will be deleted - this cannot be undone! + Todas as mensagens serão deletadas - isto não pode ser desfeito! + + + Allow to send files and media. + Permitir o envio de arquivos e mídia. + + + Allow to send SimpleX links. + Permitir envio de links SimpleX. + + + Block for all + Bloquear para todos + + + Block member + Bloquear membro + + + Blocked by admin + Bloqueado por um administrador + + + Block group members + Bloquear membros de grupo + + + Block member for all? + Bloquear membro para todos? + + + Block member? + Bloquear membro? + + + Both you and your contact can irreversibly delete sent messages. (24 hours) + Você e seu contato podem apagar mensagens enviadas de maneira irreversível. (24 horas) + + + Can't call member + Não foi possível ligar para este membro + + + Can't message member + Não foi possível enviar mensagem para este membro + + + Cancel migration + Cancelar migração + + + Abort + Abortar + + + Abort changing address + Abortar troca de endereço + + + Abort changing address? + Abortar troca de endereço? + + + - optionally notify deleted contacts. +- profile names with spaces. +- and more! + - notificar contatos apagados de maneira opcional. +- nome de perfil com espaços. +- e muito mais! + + + Allow sharing + Permitir compartilhamento + + + Block + Bloquear + + + Additional accent 2 + Esquema adicional 2 + + + Address change will be aborted. Old receiving address will be used. + Alteração de endereço será abortada. O endereço antigo será utilizado. + + + Advanced settings + Configurações avançadas + + + All data is kept private on your device. + Toda informação é privada em seu dispositivo. + + + All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + Todos os seus contatos, conversas e arquivos serão encriptados e enviados em pedaços para nós XFTP. + + + Allow irreversible message deletion only if your contact allows it to you. (24 hours) + Permitir deletar mensagens de maneira irreversível apenas se seu contato permitir para você. (24 horas) + + + Already connecting! + Já está conectando! + + + Already joining the group! + Já está entrando no grupo! + + + Always use private routing. + Sempre use rotas privadas. + + + Apply to + Aplicar em + + + Archiving database + Arquivando banco de dados + + + Black + Preto + + + Cannot forward message + Não é possível encaminhar mensagem + + + (new) + (novo) + + + (this device v%@) + este dispositivo + + + **Create 1-time link**: to create and share a new invitation link. + **Adicionar contato**: criar um novo link de convite ou conectar via um link que você recebeu. + + + **Create group**: to create a new group. + **Criar grupo**: criar um novo grupo. + + + **Warning**: the archive will be removed. + **Aviso**: o arquivo será removido. + + + A few more things + E mais algumas coisas + + + Archived contacts + Contatos arquivados + + + Cellular + Rede móvel + + + %d file(s) failed to download. + %d arquivo(s) falharam ao ser baixados. + + + %d file(s) were deleted. + %d arquivo(s) foram apagados. + + + %d messages not forwarded + %d mensagens não encaminhadas + + + Bad desktop address + Endereço de desktop incorreto + + + Blur for better privacy. + Borrar para melhor privacidade. + + + %d file(s) are still being downloaded. + %d arquivo(s) ainda estão sendo baixados. + + + %d file(s) were not downloaded. + %d arquivo(s) não foram baixados. + + + Chat colors + Cores do chat + + + %lld group events + %lld eventos do grupo + + + %lld messages moderated by %@ + %lld mensagens moderadas por %@ + + + %1$@, %2$@ + %1$@, %2$@ + + + %lld new interface languages + %lld novos idiomas de interface + + + Better networking + Melhores redes + + + 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 e Ucraniano - obrigado aos usuários e [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! + + + Better groups + Melhores grupos + + + Capacity exceeded - recipient did not receive previously sent messages. + Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente. + + + Chat migrated! + Conversa migrada! + + + Auto-accept settings + Aceitar automaticamente configurações + + + App encrypts new local files (except videos). + O aplicativo criptografa novos arquivos locais (exceto videos). + + + App session + Sessão do aplicativo + + + Acknowledged + Reconhecido + + + Acknowledgement errors + Erros conhecidos + + + Chat list + Lista de conversas + + + Chat database exported + Banco de dados da conversa exportado + + + Chat preferences were changed. + As preferências de bate-papo foram alteradas. + + + Chat theme + Tema da conversa + + + Better calls + Chamadas melhores + + + Better user experience + Melhor experiência do usuário + + + Allow downgrade + Permitir redução + + + Additional secondary + Secundária adicional + + + App data migration + Migração de dados do aplicativo + + + Archive and upload + Arquivar e enviar + + + Background + Fundo + + + Better message dates. + Datas de mensagens melhores. + + + Better notifications + Notificações melhores + + + Better security ✅ + Melhor segurança ✅ + + + Chat profile + Perfil da conversa + 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 c9c9707c39..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 @@ - + + + No comment provided by engineer. @@ -50,16 +52,19 @@ Available in v5.1 #secreto# No comment provided by engineer. - + %@ + %@ No comment provided by engineer. - + %@ %@ + %@ %@ No comment provided by engineer. - + %@ / %@ + %@ / %@ No comment provided by engineer. @@ -117,12 +122,14 @@ Available in v5.1 %d mensagem(s) ignorada(s) integrity error chat item - + %lld + %lld No comment provided by engineer. - + %lld %@ + %lld %@ No comment provided by engineer. @@ -155,24 +162,29 @@ Available in v5.1 %lld segundos 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. @@ -193,30 +205,26 @@ Available in v5.1 %u mensagens ignoradas. No comment provided by engineer. - + ( + ( No comment provided by engineer. ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Adicionar novo contato**: para criar seu QR Code único ou link para seu contato. - No comment provided by engineer. - **Create link / QR code** for your contact to use. 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. + + **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. **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. 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). + + **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. **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). No comment provided by engineer. @@ -229,8 +237,8 @@ Available in v5.1 **Atenção**: Você NÃO poderá recuperar ou alterar a senha caso a perca. 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. + + **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**: 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 é. No comment provided by engineer. @@ -359,8 +367,8 @@ Available in v5.1 Add servers by scanning QR codes. No comment provided by engineer. - - Add server… + + Add server No comment provided by engineer. @@ -1219,8 +1227,8 @@ Available in v5.1 Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1235,8 +1243,8 @@ Available in v5.1 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. @@ -1671,16 +1679,16 @@ Available in v5.1 Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1799,8 +1807,8 @@ Available in v5.1 Immediately No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1912,8 +1920,8 @@ Available in v5.1 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. @@ -2107,8 +2115,8 @@ Available in v5.1 Migration is completed No comment provided by engineer. - - Migrations: %@ + + Migrations: No comment provided by engineer. @@ -2265,8 +2273,8 @@ Available in v5.1 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**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2325,8 +2333,8 @@ Available in v5.1 Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2381,8 +2389,8 @@ Available in v5.1 Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -3085,8 +3093,8 @@ Available in v5.1 Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -3130,16 +3138,16 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of 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. @@ -3202,8 +3210,8 @@ It can happen because of some bug or when the connection is compromised.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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3428,8 +3436,8 @@ To connect, please ask your contact to create another connection link and check 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. @@ -3569,10 +3577,6 @@ SimpleX Lock must be enabled. You can't send messages! 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. @@ -4289,8 +4293,8 @@ SimpleX servers cannot see your profile. %lld novas interface de idiomas No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **Adicionar contato**: para criar um novo link de convite ou conectar-se por meio de um link que você recebeu. No comment provided by engineer. @@ -4540,6 +4544,31 @@ SimpleX servers cannot see your profile. Confirmar envio No comment provided by engineer. + + %@ downloaded + %@ baixado + No comment provided by engineer. + + + # %@ + # %@ + copied message info title, # <title> + + + %@: + %@: + copied message info + + + %@ (current) + %@(atual) + No comment provided by engineer. + + + %@ (current): + %@ (atual): + copied message info + 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 ba1ac7a929..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 @@
- +
- - - - - - 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) (можно скопировать) @@ -127,6 +100,11 @@ %@ подтверждён No comment provided by engineer. + + %@ server + %@ сервер + No comment provided by engineer. + %@ servers %@ серверы @@ -142,6 +120,11 @@ %@ хочет соединиться! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ и %lld членов группы @@ -149,7 +132,7 @@ %@, %@ and %lld other members connected - %@, %@ и %lld других членов соединены + установлено соединение с %@, %@ и %lld другими членами группы No comment provided by engineer. @@ -162,11 +145,36 @@ %d дней time interval + + %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 файлов не было загружено. + forward confirmation reason + %d hours %d ч. time interval + + %d messages not forwarded + %d сообщений не переслано + alert title + %d min %d мин @@ -182,6 +190,11 @@ %d сек time interval + + %d seconds(s) + %d секунд + delete after time + %d skipped message(s) %d пропущенных сообщение(й) @@ -219,7 +232,7 @@ %lld members - Членов группы: %lld + %lld членов No comment provided by engineer. @@ -252,11 +265,6 @@ %lld новых языков интерфейса No comment provided by engineer. - - %lld second(s) - %lld секунд - No comment provided by engineer. - %lld seconds %lld секунд @@ -307,11 +315,6 @@ %u сообщений пропущено. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (новое) @@ -322,19 +325,9 @@ (это устройство v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Добавить контакт**: создать новую ссылку-приглашение или подключиться через полученную ссылку. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Добавить новый контакт**: чтобы создать одноразовый QR код или ссылку для Вашего контакта. + + **Create 1-time link**: to create and share a new invitation link. + **Добавить контакт**: создать и поделиться новой ссылкой-приглашением. No comment provided by engineer. @@ -342,14 +335,14 @@ **Создать группу**: создать новую группу. 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. @@ -362,11 +355,16 @@ **Внимание**: Вы не сможете восстановить или поменять пароль, если Вы его потеряете. 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. + + **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 / Paste link**: to connect via a link you received. + **Сканировать / Вставить ссылку**: чтобы соединиться через полученную ссылку. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain. @@ -392,11 +390,6 @@ \*жирный* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +426,6 @@ - история редактирования. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -451,7 +439,8 @@ 1 day 1 день - time interval + delete after time +time interval 1 hour @@ -466,12 +455,29 @@ 1 month 1 месяц - time interval + delete after time +time interval 1 week 1 неделю - time interval + delete after time +time interval + + + 1 year + 1 год + delete after time + + + 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 @@ -488,11 +494,6 @@ 30 секунд No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -523,8 +524,8 @@ A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail. - Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться **для каждого контакта и члена группы**. -**Обратите внимание**: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать. + Будет использовано отдельное TCP соединение **для каждого контакта и члена группы**. +**Примечание**: Чем больше подключений, тем быстрее разряжается батарея и расходуется трафик, а некоторые соединения могут отваливаться. No comment provided by engineer. @@ -542,31 +543,32 @@ Прекратить изменение адреса? No comment provided by engineer. - - About SimpleX - О SimpleX - No comment provided by engineer. - About SimpleX Chat Информация о SimpleX Chat No comment provided by engineer. - - About SimpleX address - Об адресе SimpleX + + About operators + Об операторах No comment provided by engineer. - - Accent color - Основной цвет + + Accent + Акцент No comment provided by engineer. Accept Принять accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Принять условия + No comment provided by engineer. Accept connection request? @@ -581,21 +583,47 @@ Accept incognito Принять инкогнито - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Принятые условия + No comment provided by engineer. + + + Acknowledged + Подтверждено + No comment provided by engineer. + + + Acknowledgement errors + Ошибки подтверждения + No comment provided by engineer. + + + Active + Активный + token status text + + + 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 contact - Добавить контакт + + Add friends + Добавить друзей No comment provided by engineer. - - Add preset servers - Добавить серверы по умолчанию + + Add list + Добавить список No comment provided by engineer. @@ -603,14 +631,19 @@ Добавить профиль No comment provided by engineer. + + Add server + Добавить сервер + No comment provided by engineer. + Add servers by scanning QR codes. Добавить серверы через QR код. No comment provided by engineer. - - Add server… - Добавить сервер… + + Add team members + Добавить сотрудников No comment provided by engineer. @@ -618,11 +651,46 @@ Добавить на другое устройство 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 Адрес @@ -633,6 +701,16 @@ Изменение адреса будет прекращено. Будет использоваться старый адрес. No comment provided by engineer. + + Address or 1-time link? + Адрес или одноразовая ссылка? + No comment provided by engineer. + + + Address settings + Настройки адреса + No comment provided by engineer. + Admins can block a member for all. Админы могут заблокировать члена группы. @@ -648,6 +726,16 @@ Настройки сети No comment provided by engineer. + + Advanced settings + Настройки сети + No comment provided by engineer. + + + All + Все + No comment provided by engineer. + All app data is deleted. Все данные приложения будут удалены. @@ -658,14 +746,29 @@ Все чаты и сообщения будут удалены - это нельзя отменить! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + Все чаты будут удалены из списка %@, и список удален. + alert message + 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. - Все члены группы, которые соединились через эту ссылку, останутся в группе. + Все члены группы останутся соединены. + 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. @@ -683,6 +786,20 @@ Все новые сообщения от %@ будут скрыты! No comment provided by engineer. + + All profiles + Все профили + profile dropdown + + + 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. Все контакты, которые соединились через этот адрес, сохранятся. @@ -708,11 +825,21 @@ Разрешить звонки, только если их разрешает Ваш контакт. 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 downgrade + Разрешить прямую доставку + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа) @@ -730,7 +857,7 @@ Allow sending direct messages to members. - Разрешить посылать прямые сообщения членам группы. + Разрешить личные сообщения членам группы. No comment provided by engineer. @@ -738,11 +865,21 @@ Разрешить посылать исчезающие сообщения. No comment provided by engineer. + + 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 SimpleX links. Разрешить отправлять ссылки SimpleX. @@ -808,6 +945,11 @@ Вступление в группу уже начато! No comment provided by engineer. + + Always use private routing. + Всегда использовать конфиденциальную доставку. + No comment provided by engineer. + Always use relay Всегда соединяться через relay @@ -818,11 +960,21 @@ Будет создан пустой профиль чата с указанным именем, и приложение откроется в обычном режиме. No comment provided by engineer. + + Another reason + Другая причина + report reason + Answer call Принять звонок No comment provided by engineer. + + Anybody can host servers. + Кто угодно может запустить сервер. + No comment provided by engineer. + App build: %@ Сборка приложения: %@ @@ -838,6 +990,11 @@ Приложение шифрует новые локальные файлы (кроме видео). No comment provided by engineer. + + App group: + Группа приложения: + No comment provided by engineer. + App icon Иконка @@ -853,6 +1010,11 @@ Код доступа в приложение будет заменен кодом самоуничтожения. No comment provided by engineer. + + App session + Сессия приложения + No comment provided by engineer. + App version Версия приложения @@ -873,11 +1035,56 @@ Применить 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? + Архивировать сообщение о нарушении? + No comment provided by engineer. + + + Archive reports + Архивировать сообщения о нарушениях + swipe action + + + Archived contacts + Архивированные контакты + No comment provided by engineer. + Archiving database Подготовка архива @@ -943,11 +1150,21 @@ Автоприем изображений No comment provided by engineer. + + Auto-accept settings + Настройки автоприема + alert title + Back Назад No comment provided by engineer. + + Background + Фон + No comment provided by engineer. + Bad desktop address Неверный адрес компьютера @@ -963,16 +1180,61 @@ Ошибка хэш сообщения 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 Заблокировать @@ -985,7 +1247,7 @@ Block group members - Блокируйте членов группы + Заблокировать членов группы No comment provided by engineer. @@ -995,7 +1257,7 @@ Block member for all? - Заблокировать члена для всех? + Заблокировать для всех? No comment provided by engineer. @@ -1008,6 +1270,16 @@ Заблокирован администратором No comment provided by engineer. + + Blur for better privacy. + Размыть для конфиденциальности. + No comment provided by engineer. + + + Blur media + Размытие изображений + No comment provided by engineer. + Both you and your contact can add message reactions. И Вы, и Ваш контакт можете добавлять реакции на сообщения. @@ -1038,11 +1310,35 @@ Болгарский, финский, тайский и украинский - благодаря пользователям и [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: +- send only legal content in public groups. +- respect other users – no spam. + Используя SimpleX Chat, Вы согласны: +- отправлять только законные сообщения в публичных группах. +- уважать других пользователей – не отправлять спам. + No comment provided by engineer. + Call already ended! Звонок уже завершен! @@ -1053,11 +1349,26 @@ Звонки No comment provided by engineer. + + 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! Нельзя пригласить контакт! @@ -1068,10 +1379,16 @@ Нельзя пригласить контакты! No comment provided by engineer. + + Can't message member + Не удаётся отправить сообщение члену группы + No comment provided by engineer. + Cancel Отменить - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1400,20 @@ Ошибка доступа к Keychain при сохранении пароля No comment provided by engineer. + + Cannot forward message + Невозможно переслать сообщение + No comment provided by engineer. + Cannot receive file Невозможно получить файл - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Превышено количество сообщений - предыдущие сообщения не доставлены. + snd error text Cellular @@ -1098,6 +1425,16 @@ Поменять No comment provided by engineer. + + Change automatic message deletion? + Измененить автоматическое удаление сообщений? + alert title + + + Change chat profiles + Поменять профили + authentication reason + Change database passphrase? Поменять пароль базы данных? @@ -1142,11 +1479,26 @@ Change self-destruct passcode Изменить код самоуничтожения authentication reason - set passcode view +set passcode view - - 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. @@ -1164,6 +1516,11 @@ Данные чата удалены No comment provided by engineer. + + Chat database exported + Данные чата экспортированы + No comment provided by engineer. + Chat database imported Архив чата импортирован @@ -1184,6 +1541,11 @@ Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата. No comment provided by engineer. + + Chat list + Список чатов + No comment provided by engineer. + Chat migrated! Чат мигрирован! @@ -1194,15 +1556,50 @@ Предпочтения No comment provided by engineer. + + Chat preferences were changed. + Настройки чата были изменены. + alert message + + + 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. + Проверять сообщения по возможности. + No comment provided by engineer. + Check server address and try again. Проверьте адрес сервера и попробуйте снова. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1621,25 @@ Выбрать из библиотеки No comment provided by engineer. + + Chunks deleted + Блоков удалено + No comment provided by engineer. + + + Chunks downloaded + Блоков принято + No comment provided by engineer. + + + Chunks uploaded + Блоков загружено + No comment provided by engineer. + Clear Очистить - No comment provided by engineer. + swipe action Clear conversation @@ -1239,6 +1651,16 @@ Очистить разговор? No comment provided by engineer. + + Clear group? + Очистить группу? + No comment provided by engineer. + + + Clear or delete group? + Очистить или удалить группу? + No comment provided by engineer. + Clear private notes? Очистить личные заметки? @@ -1249,11 +1671,21 @@ Сбросить подтверждение No comment provided by engineer. - - Colors - Цвета + + Color chats with the new themes. + Добавьте цвета к чатам в настройках. No comment provided by engineer. + + Color mode + Режим цветов + No comment provided by engineer. + + + Community guidelines violation + Нарушение правил группы + report reason + Compare file Сравнение файла @@ -1264,11 +1696,56 @@ Сравните код безопасности с Вашими контактами. 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 Подтвердить @@ -1279,11 +1756,21 @@ Подтвердить Код 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 Подтвердите настройки сети @@ -1309,6 +1796,11 @@ Подтвердить загрузку No comment provided by engineer. + + Confirmed + Подтвержденный + token status text + Connect Соединиться @@ -1329,6 +1821,11 @@ Подключиться к компьютеру No comment provided by engineer. + + Connect to your friends faster. + Соединяйтесь с друзьями быстрее. + No comment provided by engineer. + Connect to yourself? Соединиться с самим собой? @@ -1368,16 +1865,31 @@ This is your own one-time link! Соединиться с %@ 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 to server… Устанавливается соединение с сервером… @@ -1388,6 +1900,11 @@ This is your own one-time link! Устанавливается соединение с сервером… (ошибка: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Контакт соединяется, подождите или проверьте позже! + No comment provided by engineer. + Connecting to desktop Подключение к компьютеру @@ -1398,6 +1915,16 @@ This is your own one-time link! Соединение No comment provided by engineer. + + Connection and servers status. + Состояние соединения и серверов. + No comment provided by engineer. + + + Connection blocked + Соединение заблокировано + No comment provided by engineer. + Connection error Ошибка соединения @@ -1408,11 +1935,38 @@ This is your own one-time link! Ошибка соединения (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + Соединение заблокировано сервером оператора: +%@ + 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 Подключение прервано @@ -1423,6 +1977,16 @@ This is your own one-time link! Превышено время соединения No comment provided by engineer. + + Connection with desktop stopped + Соединение с компьютером остановлено + No comment provided by engineer. + + + Connections + Соединения + No comment provided by engineer. + Contact allows Контакт разрешает @@ -1433,6 +1997,11 @@ This is your own one-time link! Существующий контакт No comment provided by engineer. + + Contact deleted! + Контакт удален! + No comment provided by engineer. + Contact hidden: Контакт скрыт: @@ -1443,9 +2012,9 @@ This is your own one-time link! Соединение с контактом установлено notification - - Contact is not connected yet! - Соединение еще не установлено! + + Contact is deleted. + Контакт удален. No comment provided by engineer. @@ -1458,6 +2027,11 @@ This is your own one-time link! Предпочтения контакта No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Контакт будет удален — это нельзя отменить! + No comment provided by engineer. + Contacts Контакты @@ -1468,21 +2042,41 @@ This is your own one-time link! Контакты могут помечать сообщения для удаления; Вы сможете просмотреть их. No comment provided by engineer. + + Content violates conditions of use + Содержание нарушает условия использования + blocking reason + Continue Продолжить No comment provided by engineer. + + Conversation deleted! + Разговор удален! + No comment provided by engineer. + Copy Скопировать - chat item action + 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 %@? Исправить имя на %@? @@ -1493,6 +2087,11 @@ This is your own one-time link! Создать No comment provided by engineer. + + Create 1-time link + Создать одноразовую ссылку + No comment provided by engineer. + Create SimpleX address Создать адрес SimpleX @@ -1503,11 +2102,6 @@ This is your own one-time link! Создайте группу, используя случайный профиль. No comment provided by engineer. - - Create an address to let people connect with you. - Создайте адрес, чтобы можно было соединиться с вами. - No comment provided by engineer. - Create file Создание файла @@ -1528,6 +2122,11 @@ This is your own one-time 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/). 💻 @@ -1553,6 +2152,11 @@ This is your own one-time link! Создать профиль No comment provided by engineer. + + Created + Создано + No comment provided by engineer. + Created at Создано @@ -1563,11 +2167,6 @@ This is your own one-time link! Создано: %@ copied message info - - Created on %@ - Дата создания %@ - No comment provided by engineer. - Creating archive link Создание ссылки на архив @@ -1583,11 +2182,21 @@ This is your own one-time link! Текущий Код 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 passphrase… Текущий пароль… No comment provided by engineer. + + Current profile + Текущий профиль + No comment provided by engineer. + Currently maximum supported file size is %@. Максимальный размер файла - %@. @@ -1598,11 +2207,26 @@ This is your own one-time link! Пользовательское время 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 ID ID базы данных @@ -1701,6 +2325,11 @@ This is your own one-time link! Данные чата будут мигрированы при перезапуске No comment provided by engineer. + + Debug delivery + Отладка доставки + No comment provided by engineer. + Decentralized Децентрализованный @@ -1714,18 +2343,19 @@ This is your own one-time link! Delete Удалить - chat item action + alert action +swipe action + + + Delete %lld messages of members? + Удалить %lld сообщений членов группы? + No comment provided by engineer. Delete %lld messages? Удалить %lld сообщений? No comment provided by engineer. - - Delete Contact - Удалить контакт - No comment provided by engineer. - Delete address Удалить адрес @@ -1751,14 +2381,14 @@ This is your own one-time link! Удалить и уведомить контакт 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. @@ -1771,6 +2401,11 @@ This is your own one-time link! Удалить профиль? No comment provided by engineer. + + Delete chat? + Удалить разговор? + No comment provided by engineer. + Delete connection Удалить соединение @@ -1781,11 +2416,9 @@ This is your own one-time link! Удалить контакт No comment provided by engineer. - - Delete contact? -This cannot be undone! - Удалить контакт? -Это не может быть отменено! + + Delete contact? + Удалить контакт? No comment provided by engineer. @@ -1848,6 +2481,11 @@ This cannot be undone! Удалить ссылку? No comment provided by engineer. + + Delete list? + Удалить список? + alert title + Delete member message? Удалить сообщение участника? @@ -1861,7 +2499,7 @@ This cannot be undone! Delete messages Удалить сообщения - No comment provided by engineer. + alert button Delete messages after @@ -1878,9 +2516,9 @@ This cannot be undone! Удалить предыдущую версию данных? No comment provided by engineer. - - Delete pending connection - Удалить соединение + + Delete or moderate up to 200 messages. + Удаляйте или модерируйте до 200 сообщений. No comment provided by engineer. @@ -1898,11 +2536,31 @@ This cannot be undone! Удаление очереди server test step + + 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 + Удалить без уведомления + No comment provided by engineer. + + + Deleted + Удалено + No comment provided by engineer. + Deleted at Удалено @@ -1913,6 +2571,16 @@ This cannot be undone! Удалено: %@ copied message info + + Deletion errors + Ошибки удаления + No comment provided by engineer. + + + Delivered even when Apple drops them. + Доставляются даже тогда, когда Apple их теряет. + No comment provided by engineer. + Delivery Доставка @@ -1948,11 +2616,41 @@ This cannot be undone! Компьютеры No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Адрес сервера назначения %@ несовместим с настройками пересылающего сервера %@. + No comment provided by engineer. + + + Destination server error: %@ + Ошибка сервера получателя: %@ + snd error text + + + 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 Инструменты разработчика @@ -1983,9 +2681,13 @@ This cannot be undone! Прямые сообщения chat feature - - 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. @@ -1998,11 +2700,26 @@ This cannot be undone! Отключить блокировку SimpleX authentication reason + + Disable automatic message deletion? + Отключить автоматическое удаление сообщений? + alert title + + + Disable delete messages + Отключить удаление сообщений + alert button + Disable for all Выключить для всех No comment provided by engineer. + + Disabled + Выключено + No comment provided by engineer. + Disappearing message Исчезающее сообщение @@ -2018,8 +2735,8 @@ This cannot be undone! Исчезающие сообщения запрещены в этом чате. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Исчезающие сообщения запрещены в этой группе. No comment provided by engineer. @@ -2053,11 +2770,21 @@ This cannot be undone! Обнаружение по локальной сети 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 SimpleX for emergency calls. Не используйте SimpleX для экстренных звонков. No comment provided by engineer. + + Do NOT use private routing. + Не использовать конфиденциальную доставку. + No comment provided by engineer. + Do it later Отложить @@ -2065,7 +2792,16 @@ This cannot be undone! Do not send history to new members. - Не отправлять историю новым членам. + No comment provided by engineer. + + + Do not use credentials with proxy. + Не использовать учетные данные с прокси. + No comment provided by engineer. + + + Documents: + Документы: No comment provided by engineer. @@ -2078,11 +2814,21 @@ This cannot be undone! Не включать 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 Откатить версию и открыть чат @@ -2091,7 +2837,13 @@ This cannot be undone! Download Загрузить - chat item action + alert button +chat item action + + + Download errors + Ошибки приема + No comment provided by engineer. Download failed @@ -2103,6 +2855,21 @@ This cannot be undone! Загрузка файла server test step + + Download files + Загрузить файлы + alert action + + + Downloaded + Принято + No comment provided by engineer. + + + Downloaded files + Принятые файлы + No comment provided by engineer. + Downloading archive Загрузка архива @@ -2123,6 +2890,11 @@ This cannot be undone! Длительность No comment provided by engineer. + + E2E encrypted notifications. + E2E зашифрованные нотификации. + No comment provided by engineer. + Edit Редактировать @@ -2143,6 +2915,10 @@ This cannot be undone! Включить (кроме исключений) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock Включить блокировку SimpleX @@ -2156,7 +2932,7 @@ This cannot be undone! Enable automatic message deletion? Включить автоматическое удаление сообщений? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2979,11 @@ This cannot be undone! Включить код самоуничтожения set passcode view + + Enabled + Включено + No comment provided by engineer. + Enabled for Включено для @@ -2278,6 +3059,11 @@ This cannot be undone! Ошибка нового соглашения о шифровании. No comment provided by engineer. + + Encryption renegotiation in progress. + Выполняется повторное согласование шифрования. + No comment provided by engineer. + Enter Passcode Введите Код @@ -2343,26 +3129,35 @@ This cannot be undone! Ошибка при прекращении изменения адреса No comment provided by engineer. + + Error accepting conditions + Ошибка приема условий + alert title + Error accepting contact request Ошибка при принятии запроса на соединение No comment provided by engineer. - - Error accessing database file - Ошибка при доступе к данным чата - No comment provided by engineer. - Error adding member(s) - Ошибка при добавлении членов группы No comment provided by engineer. + + Error adding server + Ошибка добавления сервера + alert title + Error changing address Ошибка при изменении адреса No comment provided by engineer. + + Error changing connection profile + Ошибка при изменении профиля соединения + No comment provided by engineer. + Error changing role Ошибка при изменении роли @@ -2373,6 +3168,21 @@ This cannot be undone! Ошибка при изменении настройки 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 Ошибка при создании адреса @@ -2388,9 +3198,13 @@ This cannot be undone! Ошибка при создании ссылки группы No comment provided by engineer. + + Error creating list + Ошибка создания списка + alert title + Error creating member contact - Ошибка создания контакта с членом группы No comment provided by engineer. @@ -2403,6 +3217,11 @@ This cannot be undone! Ошибка создания профиля! No comment provided by engineer. + + Error creating report + Ошибка создания сообщения о нарушении + No comment provided by engineer. + Error decrypting file Ошибка расшифровки файла @@ -2423,11 +3242,6 @@ This cannot be undone! Ошибка при удалении соединения No comment provided by engineer. - - Error deleting contact - Ошибка при удалении контакта - No comment provided by engineer. - Error deleting database Ошибка при удалении данных чата @@ -2473,6 +3287,11 @@ This cannot be undone! Ошибка при экспорте архива чата No comment provided by engineer. + + Error exporting theme: %@ + Ошибка экспорта темы: %@ + No comment provided by engineer. + Error importing chat database Ошибка при импорте архива чата @@ -2483,9 +3302,14 @@ This cannot be undone! Ошибка при вступлении в группу No comment provided by engineer. - - Error loading %@ servers - Ошибка загрузки %@ серверов + + Error loading servers + Ошибка загрузки серверов + alert title + + + Error migrating settings + Ошибка миграции настроек No comment provided by engineer. @@ -2496,16 +3320,35 @@ This cannot be undone! Error receiving file Ошибка при получении файла + alert title + + + Error reconnecting server + Ошибка переподключения к серверу No comment provided by engineer. + + Error reconnecting servers + Ошибка переподключения к серверам + No comment provided by engineer. + + + Error registering for notifications + Ошибка регистрации для уведомлений + alert title + Error removing member - Ошибка при удалении члена группы No comment provided by engineer. - - Error saving %@ servers - Ошибка при сохранении %@ серверов + + Error reordering lists + Ошибка сортировки списков + alert title + + + Error resetting statistics + Ошибка сброса статистики No comment provided by engineer. @@ -2513,6 +3356,11 @@ This cannot be undone! Ошибка при сохранении ICE серверов No comment provided by engineer. + + Error saving chat list + Ошибка сохранения списка чатов + alert title + Error saving group profile Ошибка при сохранении профиля группы @@ -2528,6 +3376,11 @@ This cannot be undone! Ошибка сохранения пароля в Keychain No comment provided by engineer. + + Error saving servers + Ошибка сохранения серверов + alert title + Error saving settings Ошибка сохранения настроек @@ -2550,7 +3403,6 @@ This cannot be undone! Error sending member contact invitation - Ошибка отправки приглашения члену группы No comment provided by engineer. @@ -2573,16 +3425,26 @@ This cannot be undone! Ошибка при остановке чата No comment provided by engineer. + + Error switching profile + Ошибка переключения профиля + No comment provided by engineer. + Error switching profile! Ошибка выбора профиля! - No comment provided by engineer. + alertTitle Error synchronizing connection Ошибка синхронизации соединения No comment provided by engineer. + + Error testing server connection + Ошибка проверки соединения с сервером + No comment provided by engineer. + Error updating group link Ошибка обновления ссылки группы @@ -2593,6 +3455,11 @@ This cannot be undone! Ошибка при обновлении сообщения No comment provided by engineer. + + Error updating server + Ошибка сохранения сервера + alert title + Error updating settings Ошибка при сохранении настроек сети @@ -2621,7 +3488,9 @@ This cannot be undone! Error: %@ Ошибка: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3502,16 @@ This cannot be undone! Ошибка: данные чата не найдены No comment provided by engineer. + + Errors + Ошибки + No comment provided by engineer. + + + Errors in servers configuration. + Ошибки в настройках серверов. + servers error + Even when disabled in the conversation. Даже когда они выключены в разговоре. @@ -2648,6 +3527,11 @@ This cannot be undone! Раскрыть chat item action + + Expired + Истекший + token status text + Export database Экспорт архива чата @@ -2658,6 +3542,11 @@ This cannot be undone! Ошибка при экспорте: No comment provided by engineer. + + Export theme + Экспорт темы + No comment provided by engineer. + Exported database archive. Архив чата экспортирован. @@ -2683,16 +3572,70 @@ This cannot be undone! Быстрые и не нужно ждать, когда отправитель онлайн! 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. + Ускорена отправка сообщений. + No comment provided by engineer. + Favorite Избранный + swipe action + + + Favorites + Избранное No comment provided by engineer. + + File error + Ошибка файла + file error alert title + + + File errors: +%@ + Ошибки файлов: +%@ + alert message + + + File is blocked by server operator: +%@. + Файл заблокирован оператором сервера: +%@. + file error text + + + File not found - most likely file was deleted or cancelled. + Файл не найден - скорее всего, файл был удален или отменен. + file error text + + + File server error: %@ + Ошибка сервера файлов: %@ + file error text + + + File status + Статус файла + No comment provided by engineer. + + + File status: %@ + Статус файла: %@ + copied message info + File will be deleted from servers. Файл будет удалён с серверов. @@ -2713,6 +3656,11 @@ This cannot be undone! Файл: %@ No comment provided by engineer. + + Files + Файлы + No comment provided by engineer. + Files & media Файлы и медиа @@ -2723,8 +3671,8 @@ This cannot be undone! Файлы и медиа chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Файлы и медиа запрещены в этой группе. No comment provided by engineer. @@ -2790,24 +3738,73 @@ This cannot be undone! Fix not supported by group member - Починка не поддерживается членом группы No comment provided by engineer. + + For all moderators + Для всех модераторов + No comment provided by engineer. + + + For chat profile %@: + Для профиля чата %@: + servers error + 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 + Для социальных сетей + No comment provided by engineer. + Forward Переслать chat item action + + Forward %d message(s)? + Переслать %d сообщение(й)? + alert title + Forward and save messages Переслать и сохранить сообщение No comment provided by engineer. + + Forward messages + Переслать сообщения + alert action + + + Forward messages without files? + Переслать сообщения без файлов? + alert message + + + Forward up to 20 messages at once. + Пересылайте до 20 сообщений за раз. + No comment provided by engineer. + Forwarded Переслано @@ -2818,6 +3815,40 @@ This cannot be undone! Переслано из 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: %@. + Версия пересылающего сервера несовместима с настройками сети: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Пересылающий сервер: %1$@ +Ошибка сервера получателя: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Пересылающий сервер: %1$@ +Ошибка: %2$@ + snd error text + Found desktop Компьютер найден @@ -2838,14 +3869,8 @@ This cannot be undone! Полное имя (не обязательно) No comment provided by engineer. - - Full name: - Полное имя: - No comment provided by engineer. - Fully decentralized – visible only to members. - Группа полностью децентрализована – она видна только членам. No comment provided by engineer. @@ -2863,6 +3888,21 @@ This cannot be undone! ГИФ файлы и стикеры No comment provided by engineer. + + Get notified when mentioned. + Уведомления, когда Вас упомянули. + No comment provided by engineer. + + + Good afternoon! + Добрый день! + message preview + + + Good morning! + Доброе утро! + message preview + Group Группа @@ -2918,41 +3958,6 @@ This cannot be undone! Ссылки групп No comment provided by engineer. - - Group members can add message reactions. - Члены группы могут добавлять реакции на сообщения. - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. (24 hours) - Члены группы могут необратимо удалять отправленные сообщения. (24 часа) - No comment provided by engineer. - - - Group members can send SimpleX links. - Члены группы могут отправлять ссылки SimpleX. - 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. - Члены группы могут отправлять голосовые сообщения. - No comment provided by engineer. - Group message: Групповое сообщение: @@ -2975,7 +3980,6 @@ This cannot be undone! Group profile is stored on members' devices, not on the servers. - Профиль группы хранится на устройствах членов, а не на серверах. No comment provided by engineer. @@ -2985,7 +3989,6 @@ This cannot be undone! Group will be deleted for all members - this cannot be undone! - Группа будет удалена для всех членов - это действие нельзя отменить! No comment provided by engineer. @@ -2993,11 +3996,21 @@ 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 Скрытое @@ -3040,7 +4053,6 @@ This cannot be undone! History is not sent to new members. - История не отправляется новым членам. No comment provided by engineer. @@ -3048,10 +4060,20 @@ This cannot be undone! Как SimpleX работает No comment provided by engineer. + + How it affects privacy + Как это влияет на конфиденциальность + No comment provided by engineer. + + + How it helps privacy + Как это улучшает конфиденциальность + No comment provided by engineer. + How it works Как это работает - No comment provided by engineer. + alert button How to @@ -3060,7 +4082,7 @@ This cannot be undone! How to use it - Как использовать + Про адрес No comment provided by engineer. @@ -3078,6 +4100,11 @@ This cannot be undone! ICE серверы (один на строке) No comment provided by engineer. + + IP address + IP адрес + No comment provided by engineer. + If you can't meet in person, show QR code in a video call, or share the link. Если Вы не можете встретиться лично, покажите QR-код во время видеозвонка или поделитесь ссылкой. @@ -3118,8 +4145,8 @@ This cannot be undone! Сразу No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Защищен от спама No comment provided by engineer. @@ -3143,11 +4170,22 @@ This cannot be undone! Ошибка импорта No comment provided by engineer. + + Import theme + Импорт темы + No comment provided by engineer. + Importing archive Импорт архива No comment provided by engineer. + + Improved delivery, reduced traffic usage. +More improvements are coming soon! + Улучшенная доставка, меньше трафик. + No comment provided by engineer. + Improved message delivery Улучшенная доставка сообщений @@ -3178,6 +4216,16 @@ This cannot be undone! Звуки во время звонков No comment provided by engineer. + + Inappropriate content + Неприемлемый контент + report reason + + + Inappropriate profile + Неприемлемый профиль + report reason + Incognito Инкогнито @@ -3248,6 +4296,11 @@ This cannot be undone! [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! @@ -3255,16 +4308,41 @@ This cannot be undone! No comment provided by engineer. - - Instantly - Мгновенно - No comment provided by engineer. - Interface Интерфейс No comment provided by engineer. + + Interface colors + Цвета интерфейса + No comment provided by engineer. + + + Invalid + Недействительный + token status text + + + Invalid (bad token) + Недействительный (плохой токен) + token status text + + + Invalid (expired) + Недействительный (истекший) + token status text + + + Invalid (unregistered) + Недействительный (незарегистрированный) + token status text + + + Invalid (wrong topic) + Недействительный (плохой заголовок) + token status text + Invalid QR code Неверный QR код @@ -3303,7 +4381,7 @@ This cannot be undone! Invalid server address! Ошибка в адресе сервера! - No comment provided by engineer. + alert title Invalid status @@ -3322,7 +4400,11 @@ This cannot be undone! Invite members - Пригласить членов группы + No comment provided by engineer. + + + Invite to chat + Пригласить в разговор No comment provided by engineer. @@ -3340,8 +4422,8 @@ This cannot be undone! Необратимое удаление сообщений запрещено в этом чате. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Необратимое удаление сообщений запрещено в этой группе. No comment provided by engineer. @@ -3366,6 +4448,11 @@ This cannot be undone! 3. Соединение компроментировано. 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 (%@). Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@). @@ -3384,7 +4471,7 @@ This cannot be undone! Join Вступить - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4513,11 @@ This is your link for group %@! Keep Оставить + alert action + + + Keep conversation + Оставить разговор No comment provided by engineer. @@ -3436,7 +4528,7 @@ This is your link for group %@! Keep unused invitation? Оставить неиспользованное приглашение? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4563,16 @@ This is your link for group %@! Leave Выйти + swipe action + + + Leave chat + Покинуть разговор + No comment provided by engineer. + + + Leave chat? + Покинуть разговор? No comment provided by engineer. @@ -3513,6 +4615,21 @@ This is your link for group %@! Связанные компьютеры No comment provided by engineer. + + List + Список + swipe action + + + List name and emoji should be different for all lists. + Название списка и эмодзи должны быть разными для всех списков. + No comment provided by engineer. + + + List name... + Имя списка... + No comment provided by engineer. + Live message! Живое сообщение! @@ -3523,11 +4640,6 @@ This is your link for group %@! "Живые" сообщения No comment provided by engineer. - - Local - Локальные - No comment provided by engineer. - Local name Локальное имя @@ -3548,11 +4660,6 @@ This is your link for group %@! Режим блокировки No comment provided by engineer. - - Make a private connection - Добавьте контакт - No comment provided by engineer. - Make one message disappear Одно исчезающее сообщение @@ -3563,21 +4670,11 @@ This is your link for group %@! Сделайте профиль скрытым! 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 Пометить как удаленное для всех @@ -3603,24 +4700,89 @@ This is your link for group %@! Макс. 30 секунд, доставляются мгновенно. No comment provided by engineer. + + Media & file servers + Серверы файлов и медиа + No comment provided by engineer. + + + Medium + Среднее + blur media + Member - Член группы + No comment provided by engineer. + + + Member inactive + item status text + + + Member reports + Сообщения о нарушениях + chat feature + + + 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. + No comment provided by engineer. + + + Members can send SimpleX links. + 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. + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + Меню No comment provided by engineer. @@ -3633,11 +4795,30 @@ This is your link for group %@! Отчеты о доставке сообщений! No comment provided by engineer. + + Message delivery warning + Предупреждение доставки сообщения + item status text + Message draft Черновик сообщения No comment provided by engineer. + + Message forwarded + Сообщение переслано + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + Информация об очереди сообщений + No comment provided by engineer. + Message reactions Реакции на сообщения @@ -3648,16 +4829,41 @@ This is your link for group %@! Реакции на сообщения в этом чате запрещены. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Реакции на сообщения запрещены в этой группе. 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 + Статус сообщения + No comment provided by engineer. + + + Message status: %@ + Статус сообщения: %@ + copied message info + Message text Текст сообщения @@ -3683,6 +4889,26 @@ This is your link for group %@! Сообщения от %@ будут показаны! No comment provided by engineer. + + Messages in this chat will never be deleted. + Сообщения в этом чате никогда не будут удалены. + alert message + + + Messages received + Получено сообщений + No comment provided by engineer. + + + Messages sent + Сообщений отправлено + No comment provided by engineer. + + + Messages were deleted after you selected them. + Сообщения были удалены после того, как вы их выбрали. + alert message + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Сообщения, файлы и звонки защищены **end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома. @@ -3748,9 +4974,9 @@ This is your link for group %@! Перемещение данных завершено No comment provided by engineer. - - Migrations: %@ - Миграции: %@ + + Migrations: + Миграции: No comment provided by engineer. @@ -3768,6 +4994,11 @@ This is your link for group %@! Модерировано: %@ copied message info + + More + Больше + swipe action + More improvements are coming soon! Дополнительные улучшения скоро! @@ -3778,16 +5009,16 @@ This is your link for group %@! Более надежное соединение с сетью. No comment provided by engineer. + + More reliable notifications + Более надежные уведомления + No comment provided by engineer. + Most likely this connection is deleted. Скорее всего, соединение удалено. item status description - - Most likely this contact has deleted the connection with you. - Скорее всего, этот контакт удалил соединение с Вами. - No comment provided by engineer. - Multiple chat profiles Много профилей чата @@ -3796,7 +5027,12 @@ This is your link for group %@! Mute Без звука - No comment provided by engineer. + notification label action + + + Mute all + Все без звука + notification label action Muted when inactive! @@ -3806,7 +5042,7 @@ This is your link for group %@! Name Имя - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +5054,26 @@ This is your link for group %@! Интернет-соединение No comment provided by engineer. + + Network decentralization + Децентрализация сети + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Ошибка сети - сообщение не было отправлено после многократных попыток. + snd error text + Network management Статус сети No comment provided by engineer. + + Network operator + Оператор сети + No comment provided by engineer. + Network settings Настройки сети @@ -3833,16 +5084,36 @@ This is your link for group %@! Состояние сети No comment provided by engineer. + + New + Новый + token status text + New Passcode Новый Код 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 будут использоваться для каждого сервера. + No comment provided by engineer. + New chat Новый чат No comment provided by engineer. + + New chat experience 🎉 + Новый интерфейс 🎉 + No comment provided by engineer. + New contact request Новый запрос на соединение @@ -3853,11 +5124,6 @@ This is your link for group %@! Новый контакт: notification - - New database archive - Новый архив чата - No comment provided by engineer. - New desktop app! Приложение для компьютера! @@ -3868,14 +5134,23 @@ This is your link for group %@! Новое имя No comment provided by engineer. + + New events + Новые события + notification + New in %@ Новое в %@ No comment provided by engineer. + + New media options + Новые медиа-опции + No comment provided by engineer. + New member role - Роль члена группы No comment provided by engineer. @@ -3888,6 +5163,11 @@ This is your link for group %@! Новый пароль… No comment provided by engineer. + + New server + Новый сервер + No comment provided by engineer. + No Нет @@ -3898,6 +5178,21 @@ This is your link for group %@! Нет кода доступа Authentication unavailable + + 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 Контакты не выбраны @@ -3918,6 +5213,11 @@ This is your link for group %@! Отсутствует токен устройства! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Прямого соединения пока нет, сообщение переслано или будет переслано админом. + item status description + No filtered chats Нет отфильтрованных разговоров @@ -3933,26 +5233,111 @@ This is your link for group %@! Нет истории No comment provided by engineer. + + No info, try to reload + Нет информации, попробуйте перезагрузить + No comment provided by engineer. + + + No media & file servers. + Нет серверов файлов и медиа. + servers error + + + No message + Нет сообщения + No comment provided by engineer. + + + No message servers. + Нет серверов сообщений. + servers error + 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 Нет полученных или отправленных файлов No comment provided by engineer. + + 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. + Нет серверов для отправки файлов. + servers error + + + No token! + Нет токена! + alert title + + + 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 + Ничего не выбрано + No comment provided by engineer. + + + Nothing to forward! + Нет сообщений, которые можно переслать! + alert title + Notifications Уведомления @@ -3963,13 +5348,25 @@ This is your link for group %@! Уведомления выключены No comment provided by engineer. + + Notifications error + Ошибка уведомлений + alert title + + + Notifications privacy + Конфиденциальность уведомлений + No comment provided by engineer. + + + Notifications status + Статус уведомлений + alert title + Now admins can: - delete members' messages. - disable members ("observer" role) - Теперь админы могут: -- удалять сообщения членов. -- приостанавливать членов (роль "наблюдатель") No comment provided by engineer. @@ -3980,36 +5377,35 @@ This is your link for group %@! Off Выключено - No comment provided by engineer. + blur media Ok Ок - No comment provided by engineer. + alert button Old database Предыдущая версия данных чата No comment provided by engineer. - - Old database archive - Старый архив чата - 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. +Requires compatible VPN. + Подключаться только к **onion** хостам. +Требуется совместимый 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. +Requires compatible VPN. + Onion хосты используются, если возможно. +Требуется совместимый VPN. No comment provided by engineer. @@ -4017,9 +5413,19 @@ This is your link for group %@! 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. @@ -4037,6 +5443,16 @@ This is your link for group %@! Только владельцы группы могут разрешить голосовые сообщения. 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. Только Вы можете добавлять реакции на сообщения. @@ -4090,13 +5506,18 @@ This is your link for group %@! Open Открыть - No comment provided by engineer. + alert action Open Settings Открыть Настройки No comment provided by engineer. + + Open changes + Открыть изменения + No comment provided by engineer. + Open chat Открыть чат @@ -4107,31 +5528,45 @@ This is your link for group %@! Открыть консоль authentication reason + + Open conditions + Открыть условия + No comment provided by engineer. + Open group Открыть группу No comment provided by engineer. + + Open link? + alert title + Open migration to another device Открытие миграции на другое устройство authentication reason - - Open user profiles - Открыть профили пользователя - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Открытый протокол и код - кто угодно может запустить сервер. - No comment provided by engineer. - Opening app… Приложение отрывается… No comment provided by engineer. + + Operator + Оператор + No comment provided by engineer. + + + Operator server + Сервер оператора + alert title + + + Or import archive file + Или импортировать файл архива + No comment provided by engineer. + Or paste archive link Или вставьте ссылку архива @@ -4152,11 +5587,28 @@ This is your link for group %@! Или покажите этот код No comment provided by engineer. + + Or to share privately + Или поделиться конфиденциально + No comment provided by engineer. + + + Organize chats into lists + Организуйте чаты в списки + No comment provided by engineer. + Other Другaя сеть No comment provided by engineer. + + Other file errors: +%@ + Другие ошибки файлов: +%@ + alert message + PING count Количество PING @@ -4192,6 +5644,11 @@ This is your link for group %@! Код доступа установлен! No comment provided by engineer. + + Password + Пароль + No comment provided by engineer. + Password to show Пароль чтобы раскрыть @@ -4199,7 +5656,6 @@ This is your link for group %@! Past member %@ - Бывший член %@ past/unknown group member @@ -4222,13 +5678,13 @@ This is your link for group %@! Вставьте полученную ссылку No comment provided by engineer. - - People can connect to you only via the links you share. - С Вами можно соединиться только через созданные Вами ссылки. + + Pending + Ожидает No comment provided by engineer. - - Periodically + + Periodic Периодически No comment provided by engineer. @@ -4242,11 +5698,28 @@ This is your link for group %@! Звонки с картинкой-в-картинке 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. +Please share any other issues with the developers. + Пожалуйста, проверьте, что мобильный и компьютер находятся в одной и той же локальной сети, и что брандмауэр компьютера разрешает подключение. +Пожалуйста, поделитесь любыми другими ошибками с разработчиками. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Пожалуйста, проверьте, что Вы использовали правильную ссылку или попросите, чтобы Ваш контакт отправил Вам другую ссылку. @@ -4314,11 +5787,31 @@ Error: %@ Пожалуйста, надежно сохраните пароль, Вы НЕ сможете его поменять, если потеряете. No comment provided by engineer. + + 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. + Пожалуйста, дождитесь регистрации токена. + token info + Polish interface Польский интерфейс No comment provided by engineer. + + Port + Порт + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Возможно, хэш сертификата в адресе сервера неверный @@ -4329,41 +5822,86 @@ Error: %@ Сохранить последний черновик, вместе с вложениями. No comment provided by engineer. - - Preset server - Сервер по умолчанию - No comment provided by engineer. - Preset server address Адрес сервера по умолчанию No comment provided by engineer. + + 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 🚀 + Конфиденциальная доставка 🚀 + No comment provided by engineer. + Private notes Личные заметки name of notes to self + + Private routing + Конфиденциальная доставка + No comment provided by engineer. + + + Private routing error + Ошибка конфиденциальной доставки + No comment provided by engineer. + Profile and server connections Профиль и соединения на сервере @@ -4379,25 +5917,20 @@ Error: %@ Картинки профилей No comment provided by engineer. - - Profile name - Имя профиля - No comment provided by engineer. - - - Profile name: - Имя профиля: - No comment provided by engineer. - Profile password Пароль профиля No comment provided by engineer. + + Profile theme + Тема профиля + No comment provided by engineer. + Profile update will be sent to your contacts. Обновлённый профиль будет отправлен Вашим контактам. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,6 +5952,11 @@ Error: %@ Запретить реакции на сообщения. No comment provided by engineer. + + Prohibit reporting messages to moderators. + Запретить жаловаться модераторам группы. + No comment provided by engineer. + Prohibit sending SimpleX links. Запретить отправку ссылок SimpleX. @@ -4426,7 +5964,6 @@ Error: %@ Prohibit sending direct messages to members. - Запретить посылать прямые сообщения членам группы. No comment provided by engineer. @@ -4444,11 +5981,23 @@ Error: %@ Запретить отправлять голосовые сообщений. No comment provided by engineer. + + Protect IP address + Защитить IP адрес + No comment provided by engineer. + Protect app screen Защитить экран приложения No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. +Включите в настройках *Сети и серверов*. + No comment provided by engineer. + Protect your chat profiles with a password! Защитите Ваши профили чата паролем! @@ -4464,6 +6013,21 @@ Error: %@ Таймаут протокола на 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 Доставка уведомлений @@ -4484,6 +6048,11 @@ Error: %@ Оценить приложение No comment provided by engineer. + + Reachable chat toolbar + Доступная панель чата + No comment provided by engineer. + React… Реакция… @@ -4492,33 +6061,28 @@ Error: %@ Read Прочитано - No comment provided by engineer. + swipe action Read more Узнать больше No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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). No comment provided by engineer. - - Read more in our GitHub repository. - Узнайте больше из нашего GitHub репозитория. - 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). @@ -4529,6 +6093,11 @@ Error: %@ Отчёты о доставке выключены No comment provided by engineer. + + Receive errors + Ошибки приема + No comment provided by engineer. + Received at Получено @@ -4549,16 +6118,26 @@ Error: %@ Полученное сообщение message info title + + 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. Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн. No comment provided by engineer. - - Receiving concurrency - Одновременный приём - No comment provided by engineer. - Receiving file will be stopped. Приём файла будет прекращён. @@ -4584,11 +6163,36 @@ Error: %@ Получатели видят их в то время как Вы их набираете. 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? Переподключить серверы? @@ -4609,10 +6213,26 @@ Error: %@ Уменьшенное потребление батареи No comment provided by engineer. + + Register + Зарегистрировать + No comment provided by engineer. + + + Register notification token? + Зарегистрировать токен уведомлений? + token info + + + Registered + Зарегистрирован + token status text + Reject Отклонить - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4639,14 +6259,22 @@ Error: %@ Удалить No comment provided by engineer. + + Remove archive? + Удалить архив? + No comment provided by engineer. + + + Remove image + Удалить изображение + No comment provided by engineer. + Remove member - Удалить члена группы No comment provided by engineer. Remove member? - Удалить члена группы? No comment provided by engineer. @@ -4699,6 +6327,56 @@ Error: %@ Ответить chat item action + + Report + Пожаловаться + chat item action + + + 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. + Пожаловаться: увидят только модераторы группы. + report reason + + + Report reason? + Причина сообщения? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + Пожаловаться на спам: увидят только модераторы группы. + report reason + + + Report violation: only group moderators will see it. + Пожаловаться на нарушение: увидят только модераторы группы. + report reason + + + Report: %@ + Сообщение о нарушении: %@ + report in notification + + + Reporting messages to moderators is prohibited. + Сообщения о нарушениях запрещены в этой группе. + No comment provided by engineer. + + + Reports + Сообщения о нарушениях + No comment provided by engineer. + Required Обязательно @@ -4709,16 +6387,41 @@ Error: %@ Сбросить 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 Перезапустите приложение, чтобы создать новый профиль. @@ -4759,9 +6462,9 @@ Error: %@ Показать chat item action - - Revert - Отменить изменения + + Review conditions + Посмотреть условия No comment provided by engineer. @@ -4789,9 +6492,19 @@ Error: %@ Запустить chat No comment provided by engineer. - - SMP servers - SMP серверы + + SMP server + SMP сервер + No comment provided by engineer. + + + SOCKS proxy + SOCKS прокси + No comment provided by engineer. + + + Safely receive files + Получайте файлы безопасно No comment provided by engineer. @@ -4802,21 +6515,26 @@ Error: %@ Save Сохранить - chat item action + alert button +chat item action Save (and notify contacts) Сохранить (и уведомить контакты) - No comment provided by engineer. + alert button Save and notify contact Сохранить и уведомить контакт - No comment provided by engineer. + alert button Save and notify group members - Сохранить и уведомить членов группы + No comment provided by engineer. + + + Save and reconnect + Сохранить и переподключиться No comment provided by engineer. @@ -4824,21 +6542,16 @@ Error: %@ Сохранить сообщение и обновить группу 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 Сохранить пароль и открыть чат @@ -4852,7 +6565,7 @@ Error: %@ Save preferences? Сохранить предпочтения? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6580,18 @@ Error: %@ Save servers? Сохранить серверы? - No comment provided by engineer. - - - Save settings? - Сохранить настройки? - No comment provided by engineer. + alert title Save welcome message? Сохранить приветственное сообщение? No comment provided by engineer. + + Save your profile? + Сохранить ваш профиль? + alert title + Saved Сохранено @@ -4899,6 +6612,21 @@ Error: %@ Сохраненное сообщение message info title + + Saving %lld messages + Сохранение %lld сообщений + No comment provided by engineer. + + + Scale + Масштаб + No comment provided by engineer. + + + Scan / Paste link + Сканировать / Вставить ссылку + No comment provided by engineer. + Scan QR code Сканировать QR код @@ -4939,11 +6667,21 @@ Error: %@ Искать или вставьте ссылку SimpleX No comment provided by engineer. + + Secondary + Вторичный + No comment provided by engineer. + Secure queue Защита очереди server test step + + Secured + Защищено + No comment provided by engineer. + Security assessment Аудит безопасности @@ -4957,6 +6695,21 @@ Error: %@ Select Выбрать + chat item action + + + 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. @@ -4994,11 +6747,6 @@ Error: %@ Отправка отчётов о доставке No comment provided by engineer. - - Send direct message - Отправить сообщение - No comment provided by engineer. - Send direct message to connect Отправьте сообщение чтобы соединиться @@ -5009,6 +6757,11 @@ Error: %@ Отправить исчезающее сообщение No comment provided by engineer. + + Send errors + Ошибки отправки + No comment provided by engineer. + Send link previews Отправлять картинки ссылок @@ -5019,14 +6772,29 @@ Error: %@ Отправить живое сообщение 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. @@ -5046,13 +6814,12 @@ Error: %@ Send up to 100 last messages to new members. - Отправить до 100 последних сообщений новым членам. No comment provided by engineer. Sender cancelled file transfer. Отправитель отменил передачу файла. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5109,6 +6876,11 @@ Error: %@ Отправлено: %@ copied message info + + Sent directly + Отправлено напрямую + No comment provided by engineer. + Sent file event Отправка файла @@ -5119,11 +6891,71 @@ Error: %@ Отправленное сообщение message info title + + 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 + Сервер + No comment provided by engineer. + + + Server added to operator %@. + Сервер добавлен к оператору %@. + alert message + + + Server address + Адрес сервера + No comment provided by engineer. + + + Server address is incompatible with network settings. + Адрес сервера несовместим с настройками сети. + srv error text. + + + Server address is incompatible with network settings: %@. + Адрес сервера несовместим с сетевыми настройками: %@. + No comment provided by engineer. + + + Server operator changed. + Оператор серверов изменен. + alert title + + + Server operators + Операторы серверов + No comment provided by engineer. + + + Server protocol changed. + Протокол сервера изменен. + alert title + Server requires authorization to create queues, check password Сервер требует авторизации для создания очередей, проверьте пароль @@ -5139,11 +6971,36 @@ Error: %@ Ошибка теста сервера! No comment provided by engineer. + + Server type + Тип сервера + No comment provided by engineer. + + + Server version is incompatible with network settings. + Версия сервера несовместима с настройками сети. + srv error text + + + 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 Код сессии @@ -5154,11 +7011,21 @@ Error: %@ Установить 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 Предпочтения группы @@ -5169,6 +7036,11 @@ Error: %@ Установите код вместо системной аутентификации. No comment provided by engineer. + + Set message expiration in chats. + Установите срок хранения сообщений в чатах. + No comment provided by engineer. + Set passcode Установить код доступа @@ -5186,7 +7058,6 @@ Error: %@ Set the message shown to new members! - Установить сообщение для новых членов группы! No comment provided by engineer. @@ -5199,6 +7070,11 @@ Error: %@ Настройки No comment provided by engineer. + + Settings were changed. + Настройки были изменены. + alert message + Shape profile images Форма картинок профилей @@ -5207,21 +7083,42 @@ Error: %@ Share Поделиться - chat item action + alert action +chat item action Share 1-time link Поделиться одноразовой ссылкой No comment provided by engineer. + + Share 1-time link with a friend + Поделитесь одноразовой ссылкой с другом + No comment provided by engineer. + + + Share SimpleX address on social media. + Поделитесь SimpleX адресом в социальных сетях. + No comment provided by engineer. + Share address Поделиться адресом No comment provided by engineer. + + Share address publicly + Поделитесь адресом + No comment provided by engineer. + Share address with contacts? Поделиться адресом с контактами? + alert title + + + Share from other apps. + Поделитесь из других приложений. No comment provided by engineer. @@ -5229,16 +7126,30 @@ Error: %@ Поделиться ссылкой No comment provided by engineer. + + Share profile + Поделиться профилем + 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. + + Short link + No comment provided by engineer. + Show QR code Показать QR код @@ -5259,21 +7170,46 @@ Error: %@ Показывать последние сообщения 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 → on messages sent via private routing. + Показать → на сообщениях доставленных конфиденциально. + 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 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. @@ -5304,6 +7240,20 @@ Error: %@ Адрес 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 channel link + simplex link type + SimpleX contact address SimpleX ссылка-контакт @@ -5324,8 +7274,8 @@ Error: %@ SimpleX ссылки chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. Ссылки SimpleX запрещены в этой группе. No comment provided by engineer. @@ -5339,11 +7289,21 @@ Error: %@ SimpleX одноразовая ссылка simplex link type + + 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 Пропустить @@ -5359,16 +7319,49 @@ Error: %@ Маленькие группы (до 20) No comment provided by engineer. + + Soft + Слабое + blur media + + + 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: + Во время импорта произошли некоторые ошибки: + No comment provided by engineer. + + + Some servers failed the test: +%@ + Серверы не прошли тест: +%@ + alert message + Somebody Контакт notification title + + Spam + Спам + blocking reason +report reason + Square, circle, or anything in between. Квадрат, круг и все, что между ними. @@ -5389,6 +7382,16 @@ Error: %@ Запустить перемещение данных No comment provided by engineer. + + Starting from %@. + Начиная с %@. + No comment provided by engineer. + + + Statistics + Статистика + No comment provided by engineer. + Stop Остановить @@ -5404,11 +7407,6 @@ Error: %@ Остановить чат 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. Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен. @@ -5437,28 +7435,63 @@ Error: %@ Stop sharing Прекратить делиться - No comment provided by engineer. + alert action Stop sharing address? Прекратить делиться адресом? - No comment provided by engineer. + alert title Stopping chat Остановка чата No comment provided by engineer. + + Storage + Хранилище + No comment provided by engineer. + + + Strong + Сильное + blur media + 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 Системная @@ -5469,11 +7502,21 @@ Error: %@ Системная аутентификация 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 @@ -5489,11 +7532,21 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Хвост + No comment provided by engineer. + Take picture Сделать фото No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. + No comment provided by engineer. + Tap button Нажмите кнопку @@ -5529,16 +7582,21 @@ Error: %@ Нажмите, чтобы сканировать No comment provided by engineer. - - Tap to start a new chat - Нажмите, чтобы начать чат - No comment provided by engineer. + + Temporary file error + Временная ошибка файла + file error alert title Test failed at step %@. Ошибка теста на шаге %@. server test failure + + Test notifications + Протестировать уведомления + No comment provided by engineer. + Test server Тестировать сервер @@ -5552,7 +7610,7 @@ Error: %@ Tests failed! Ошибка тестов! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7627,6 @@ Error: %@ Благодаря пользователям – добавьте переводы через Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Первая в мире платформа без идентификаторов пользователей. - No comment provided by engineer. - 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. @@ -5586,6 +7639,16 @@ It can happen because of some bug or when the connection is compromised.Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках. 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. Попытка поменять пароль базы данных не была завершена. @@ -5596,6 +7659,11 @@ It can happen because of some bug or when the connection is compromised.Этот 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! Подтвержденное соединение будет отменено! @@ -5616,6 +7684,11 @@ It can happen because of some bug or when the connection is compromised.Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения! No comment provided by engineer. + + The future of messaging + Будущее коммуникаций + No comment provided by engineer. + The hash of the previous message is different. Хэш предыдущего сообщения отличается. @@ -5623,17 +7696,18 @@ It can happen because of some bug or when the connection is compromised. 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 - Новое поколение приватных сообщений + + 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. @@ -5641,9 +7715,14 @@ It can happen because of some bug or when the connection is compromised.Предыдущая версия данных чата не удалена при перемещении, её можно удалить. No comment provided by engineer. - - The profile is only shared with your contacts. - Профиль отправляется только Вашим контактам. + + The same conditions will apply to operator **%@**. + Те же самые условия будут приняты для оператора **%@**. + No comment provided by engineer. + + + The second preset operator in the app! + Второй оператор серверов в приложении! No comment provided by engineer. @@ -5661,14 +7740,29 @@ It can happen because of some bug or when the connection is compromised.Серверы для новых соединений Вашего текущего профиля чата **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + Серверы для новых файлов Вашего текущего профиля **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Вставленный текст не является SimpleX-ссылкой. No comment provided by engineer. - - Theme - Тема + + 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. @@ -5691,6 +7785,11 @@ It can happen because of some bug or when the connection is compromised.Это действие нельзя отменить — все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + Это действие нельзя отменить - сообщения в этом чате, отправленные или полученные раньше чем выбрано, будут удалены. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. @@ -5718,7 +7817,6 @@ It can happen because of some bug or when the connection is compromised. This group has over %lld members, delivery receipts are not sent. - В группе более %lld членов, отчёты о доставке выключены. No comment provided by engineer. @@ -5736,11 +7834,30 @@ It can happen because of some bug or when the connection is compromised.Это ваша собственная одноразовая ссылка! 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. + 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: Чтобы задать вопросы и получать уведомления о новых версиях, @@ -5761,9 +7878,9 @@ It can happen because of some bug or when the connection is compromised.Чтобы соединиться 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. @@ -5771,6 +7888,11 @@ It can happen because of some bug or when the connection is compromised.Чтобы защитить Ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,6 +7900,26 @@ You will be prompted to complete authentication before this feature is enabled.< Вам будет нужно пройти аутентификацию для включения блокировки. 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. Для записи голосового сообщения, пожалуйста разрешите доступ к микрофону. @@ -5788,26 +7930,61 @@ You will be prompted to complete authentication before this feature is enabled.< Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**. 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. Установите режим Инкогнито при соединении. No comment provided by engineer. + + Token status: %@. + Статус токена: %@. + token status + + + 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: %@). Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %@). @@ -5850,23 +8027,20 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member - Разблокировать члена группы No comment provided by engineer. Unblock member for all? - Разблокировать члена для всех? No comment provided by engineer. Unblock member? - Разблокировать члена группы? No comment provided by engineer. - - Unexpected error: %@ - Неожиданная ошибка: %@ - item status description + + Undelivered messages + Недоставленные сообщения + No comment provided by engineer. Unexpected migration state @@ -5876,7 +8050,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. Не избр. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +8087,11 @@ You will be prompted to complete authentication before this feature is enabled.< Неизвестная ошибка No comment provided by engineer. + + Unknown servers! + Неизвестные серверы! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Если Вы не используете интерфейс iOS, включите режим Не отвлекать, чтобы звонок не прерывался. @@ -5948,16 +8127,19 @@ To connect, please ask your contact to create another connection link and check Unmute Уведомлять - No comment provided by engineer. + notification label action Unread Не прочитано + swipe action + + + Unsupported connection link No comment provided by engineer. Up to 100 last messages are sent to new members. - До 100 последних сообщений отправляются новым членам. No comment provided by engineer. @@ -5965,11 +8147,6 @@ To connect, please ask your contact to create another connection link and check Обновить No comment provided by engineer. - - Update .onion hosts setting? - Обновить настройки .onion хостов? - No comment provided by engineer. - Update database passphrase Поменять пароль @@ -5980,9 +8157,14 @@ To connect, please ask your contact to create another connection link and check Обновить настройки сети? No comment provided by engineer. - - Update transport isolation mode? - Обновить режим отдельных сессий? + + Update settings? + Обновить настройки? + No comment provided by engineer. + + + Updated conditions + Обновленные условия No comment provided by engineer. @@ -5990,16 +8172,16 @@ To connect, please ask your contact to create another connection link and check Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами. 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 Обновить и открыть чат No comment provided by engineer. + + Upload errors + Ошибки загрузки + No comment provided by engineer. + Upload failed Ошибка загрузки @@ -6010,21 +8192,50 @@ To connect, please ask your contact to create another connection link and check Загрузка файла server test step + + Uploaded + Загружено + No comment provided by engineer. + + + Uploaded files + Отправленные файлы + No comment provided by engineer. + Uploading archive Загрузка архива No comment provided by engineer. + + Use %@ + Использовать %@ + No comment provided by engineer. + Use .onion hosts Использовать .onion хосты No comment provided by engineer. + + Use SOCKS proxy + Использовать SOCKS прокси + No comment provided by engineer. + Use SimpleX Chat servers? Использовать серверы предосталенные SimpleX Chat? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + Использовать TCP-порт %@, когда порт не указан. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Использовать чат @@ -6035,6 +8246,16 @@ To connect, please ask your contact to create another connection link and check Использовать активный профиль 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 Использовать для новых соединений @@ -6060,24 +8281,53 @@ To connect, please ask your contact to create another connection link and check Использовать только локальные нотификации? 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 short links (BETA) + No comment provided by engineer. + Use the app while in the call. Используйте приложение во время звонка. No comment provided by engineer. - - User profile - Профиль чата + + Use the app with one hand. + Используйте приложение одной рукой. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Для использования .onion хостов требуется совместимый VPN провайдер. + + Use web port + Использовать веб-порт + No comment provided by engineer. + + + User selection + Выбор пользователя + No comment provided by engineer. + + + Username + Имя пользователя No comment provided by engineer. @@ -6150,11 +8400,21 @@ To connect, please ask your contact to create another connection link and check Видео и файлы до 1гб No comment provided by engineer. + + View conditions + Посмотреть условия + No comment provided by engineer. + View security code Показать код безопасности No comment provided by engineer. + + View updated conditions + Посмотреть измененные условия + No comment provided by engineer. + Visible history Доступ к истории @@ -6170,8 +8430,8 @@ To connect, please ask your contact to create another connection link and check Голосовые сообщения запрещены в этом чате. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Голосовые сообщения запрещены в этой группе. No comment provided by engineer. @@ -6210,6 +8470,16 @@ To connect, please ask your contact to create another connection link and check Ожидание видео No comment provided by engineer. + + Wallpaper accent + Рисунок обоев + No comment provided by engineer. + + + Wallpaper background + Фон обоев + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений @@ -6255,9 +8525,9 @@ To connect, please ask your contact to create another connection link and check Во время соединения аудио и видео звонков. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Когда Вы получите запрос на соединение, Вы можете принять или отклонить его. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем. No comment provided by engineer. @@ -6295,24 +8565,39 @@ To connect, please ask your contact to create another connection link and check С уменьшенным потреблением батареи. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: %@. + alert message + Wrong database passphrase Неправильный пароль базы данных No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален. + file error text + Wrong passphrase! Неправильный пароль! No comment provided by engineer. - - XFTP servers - XFTP серверы - No comment provided by engineer. - - - You - Вы + + XFTP server + XFTP сервер No comment provided by engineer. @@ -6340,6 +8625,11 @@ To connect, please ask your contact to create another connection link and check Вы уже соединены с контактом %@. No comment provided by engineer. + + You are already connected with %@. + Вы уже соединены с %@. + No comment provided by engineer. + You are already connecting to %@. Вы уже соединяетесь с %@. @@ -6387,11 +8677,26 @@ Repeat join request? Вы приглашены в группу 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 can accept calls from lock screen, without device and app authentication. Вы можете принимать звонки на экране блокировки, без аутентификации. No comment provided by engineer. + + 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 Вы можете создать его позже @@ -6422,11 +8727,21 @@ Repeat join request? Вы можете сделать его видимым для ваших контактов в SimpleX через Настройки. No comment provided by engineer. - - You can now send messages to %@ - Вы теперь можете отправлять сообщения %@ + + You can now chat with %@ + Вы теперь можете общаться с %@ notification body + + 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. Вы можете установить просмотр уведомлений на экране блокировки в настройках. @@ -6434,7 +8749,6 @@ Repeat join request? 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. @@ -6442,16 +8756,16 @@ Repeat join request? Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Вы можете использовать Ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с Вами. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Вы можете запустить чат через Настройки приложения или перезапустив приложение. No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Вы по-прежнему можете просмотреть разговор с %@ в списке чатов. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Вы можете включить Блокировку SimpleX через Настройки. @@ -6465,23 +8779,23 @@ Repeat join request? You can view invitation link again in connection details. Вы можете увидеть ссылку-приглашение снова открыв соединение. - No comment provided by engineer. + alert message You can't send messages! Вы не можете отправлять сообщения! 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 decide who can connect. + Вы определяете, кто может соединиться. + No comment provided by engineer. + You have already requested connection via this address! Вы уже запросили соединение через этот адрес! @@ -6494,11 +8808,6 @@ Repeat connection request? Повторить запрос? 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. Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата. @@ -6516,7 +8825,16 @@ Repeat connection request? You joined this group. Connecting to inviting group member. - Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы. + 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. @@ -6524,6 +8842,11 @@ Repeat connection request? Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от каких то контактов. 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. Чтобы включить отправку голосовых сообщений, разрешите их Вашему контакту. @@ -6539,6 +8862,11 @@ Repeat connection request? Вы отправили приглашение в группу No comment provided by engineer. + + You should receive notifications. + Вы должны получать уведомления. + token info + You will be connected to group when the group host's device is online, please wait or check later! Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже! @@ -6566,7 +8894,6 @@ Repeat connection request? You will connect to all group members. - Вы соединитесь со всеми членами группы. No comment provided by engineer. @@ -6574,6 +8901,11 @@ Repeat connection request? Вы все равно получите звонки и уведомления в профилях без звука, когда они активные. 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. Вы перестанете получать сообщения от этой группы. История чата будет сохранена. @@ -6594,31 +8926,16 @@ Repeat connection request? Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие Вашего основного профиля, приглашать контакты не разрешено No comment provided by engineer. - - Your %@ servers - Ваши %@ серверы - No comment provided by engineer. - Your ICE servers Ваши ICE серверы No comment provided by engineer. - - Your SMP servers - Ваши SMP серверы - No comment provided by engineer. - Your SimpleX address Ваш адрес SimpleX No comment provided by engineer. - - Your XFTP servers - Ваши XFTP серверы - No comment provided by engineer. - Your calls Ваши звонки @@ -6634,16 +8951,19 @@ Repeat connection request? База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные. No comment provided by engineer. + + Your chat preferences + Ваши настройки чата + alert title + Your chat profiles Ваши профили чата No comment provided by engineer. - - 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). - Ваш контакт должен быть в сети чтобы установить соединение. -Вы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой). + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Соединение было перемещено на %@, но при смене профиля произошла неожиданная ошибка. No comment provided by engineer. @@ -6661,6 +8981,11 @@ You can cancel this connection and remove the contact (and try later with a new Ваши контакты сохранятся. No comment provided by engineer. + + Your credentials may be sent unencrypted. + Ваши учетные данные могут быть отправлены в незашифрованном виде. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Текущие данные Вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными. @@ -6691,33 +9016,36 @@ You can cancel this connection and remove the contact (and try later with a new Будет отправлен Ваш профиль **%@**. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. -SimpleX серверы не могут получить доступ к Вашему профилю. + + Your profile is stored on your device and only shared with your contacts. + Ваш профиль храниться на Вашем устройстве и отправляется только контактам. No comment provided by engineer. - - Your profile, contacts and delivered messages are stored on your device. - Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве. + + 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 was changed. If you save it, the updated profile will be sent to all your contacts. + Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам. + alert message + Your random profile Случайный профиль No comment provided by engineer. - - Your server - Ваш сервер - No comment provided by engineer. - Your server address Адрес Вашего сервера No comment provided by engineer. + + Your servers + Ваши серверы + No comment provided by engineer. + Your settings Настройки @@ -6758,6 +9086,11 @@ SimpleX серверы не могут получить доступ к Ваше принятый звонок call status + + accepted invitation + принятое приглашение + chat list item title + admin админ @@ -6780,7 +9113,6 @@ SimpleX серверы не могут получить доступ к Ваше all members - все члены feature role @@ -6793,6 +9125,16 @@ SimpleX серверы не могут получить доступ к Ваше и %lld других событий No comment provided by engineer. + + archived report + заархивированное сообщение о нарушении + No comment provided by engineer. + + + attempts + попытки + No comment provided by engineer. + audio call (not e2e encrypted) аудиозвонок (не e2e зашифрованный) @@ -6826,13 +9168,19 @@ SimpleX серверы не могут получить доступ к Ваше blocked by admin заблокировано администратором - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold жирный No comment provided by engineer. + + call + звонок + No comment provided by engineer. + call error ошибка звонка @@ -6936,7 +9284,7 @@ SimpleX серверы не могут получить доступ к Ваше connecting… соединяется… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +9331,16 @@ SimpleX серверы не могут получить доступ к Ваше дней time unit + + decryption errors + ошибки расшифровки + No comment provided by engineer. + default (%@) по умолчанию (%@) - pref value + delete after time +pref value default (no) @@ -7033,6 +9387,11 @@ SimpleX серверы не могут получить доступ к Ваше повторное сообщение integrity error chat item + + duplicates + дубликаты + No comment provided by engineer. + e2e encrypted e2e зашифровано @@ -7108,9 +9467,9 @@ SimpleX серверы не могут получить доступ к Ваше ошибка No comment provided by engineer. - - event happened - событие произошло + + expired + истекло No comment provided by engineer. @@ -7143,6 +9502,11 @@ SimpleX серверы не могут получить доступ к Ваше Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления. No comment provided by engineer. + + inactive + неактивен + No comment provided by engineer. + incognito via contact address link инкогнито через ссылку-контакт @@ -7183,6 +9547,11 @@ SimpleX серверы не могут получить доступ к Ваше приглашение в группу %@ group name + + invite + пригласить + No comment provided by engineer. + invited приглашен(а) @@ -7225,12 +9594,10 @@ SimpleX серверы не могут получить доступ к Ваше member - член группы member role member %1$@ changed to %2$@ - член %1$@ изменился на %2$@ profile update event chat item @@ -7238,6 +9605,11 @@ SimpleX серверы не могут получить доступ к Ваше соединен(а) rcv group event chat item + + message + написать + No comment provided by engineer. + message received получено сообщение @@ -7263,6 +9635,11 @@ SimpleX серверы не могут получить доступ к Ваше удалено %@ marked deleted chat item preview text + + moderator + модератор + member role + months месяцев @@ -7271,7 +9648,7 @@ SimpleX серверы не могут получить доступ к Ваше never никогда - No comment provided by engineer. + delete after time new message @@ -7302,8 +9679,8 @@ SimpleX серверы не могут получить доступ к Ваше off нет enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9697,16 @@ SimpleX серверы не могут получить доступ к Ваше да group pref value + + other + другое + No comment provided by engineer. + + + other errors + другие ошибки + No comment provided by engineer. + owner владелец @@ -7335,6 +9722,16 @@ SimpleX серверы не могут получить доступ к Ваше peer-to-peer No comment provided by engineer. + + pending + ожидает + No comment provided by engineer. + + + pending approval + ожидает утверждения + No comment provided by engineer. + quantum resistant e2e encryption квантово-устойчивое e2e шифрование @@ -7350,6 +9747,11 @@ SimpleX серверы не могут получить доступ к Ваше получено подтверждение… No comment provided by engineer. + + rejected + отклонён + No comment provided by engineer. + rejected call отклонённый звонок @@ -7380,6 +9782,11 @@ SimpleX серверы не могут получить доступ к Ваше удалил(а) Вас из группы rcv group event chat item + + requested to connect + запрошено соединение + chat list item title + saved сохранено @@ -7390,6 +9797,11 @@ SimpleX серверы не могут получить доступ к Ваше сохранено из %@ No comment provided by engineer. + + search + поиск + No comment provided by engineer. + sec сек @@ -7415,6 +9827,15 @@ SimpleX серверы не могут получить доступ к Ваше отправьте сообщение No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + информация сервера об очереди: %1$@ + +последнее полученное сообщение: %2$@ + queue info + set new contact address установлен новый адрес контакта @@ -7455,11 +9876,21 @@ SimpleX серверы не могут получить доступ к Ваше неизвестно connection info + + unknown servers + неизвестные серверы + No comment provided by engineer. + unknown status неизвестный статус No comment provided by engineer. + + unprotected + незащищённый + No comment provided by engineer. + updated group profile обновил(а) профиль группы @@ -7500,6 +9931,11 @@ SimpleX серверы не могут получить доступ к Ваше через relay сервер No comment provided by engineer. + + video + видеозвонок + No comment provided by engineer. + video call (not e2e encrypted) видеозвонок (не e2e зашифрованный) @@ -7525,6 +9961,11 @@ SimpleX серверы не могут получить доступ к Ваше недель time unit + + when IP hidden + когда IP защищен + No comment provided by engineer. + yes да @@ -7609,7 +10050,7 @@ SimpleX серверы не могут получить доступ к Ваше
- +
@@ -7646,7 +10087,7 @@ SimpleX серверы не могут получить доступ к Ваше
- +
@@ -7666,4 +10107,249 @@ SimpleX серверы не могут получить доступ к Ваше
+ +
+ +
+ + + %d new events + %d новых сообщений + notification body + + + From %d chat(s) + notification body + + + From: %@ + От: %@ + notification body + + + New events + Новые события + notification + + + New messages + Новые сообщения + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Все права защищены. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 1f62fad60f..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 @@
- +
- - - - - - 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) (สามารถคัดลอกได้) @@ -120,9 +93,12 @@ %@ ได้รับการตรวจสอบแล้ว No comment provided by engineer. + + %@ server + No comment provided by engineer. + %@ servers - %@ เซิร์ฟเวอร์ No comment provided by engineer. @@ -134,6 +110,10 @@ %@ อยากเชื่อมต่อ! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members No comment provided by engineer. @@ -152,11 +132,31 @@ %d วัน time interval + + %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. + forward confirmation reason + %d hours %d ชั่วโมง time interval + + %d messages not forwarded + alert title + %d min %d นาที @@ -172,6 +172,10 @@ %d วินาที time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d ข้อความที่ถูกข้าม @@ -236,11 +240,6 @@ %lld new interface languages No comment provided by engineer. - - %lld second(s) - %lld วินาที - No comment provided by engineer. - %lld seconds %lld วินาที @@ -291,11 +290,6 @@ %u ข้อความที่ถูกข้าม No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) No comment provided by engineer. @@ -304,31 +298,21 @@ (this device v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ + + **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. **Create group**: to create a new group. 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. + + **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). + + **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. @@ -341,11 +325,15 @@ **โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย 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. + + **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 / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain @@ -370,11 +358,6 @@ \*ตัวหนา* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -405,11 +388,6 @@ - ประวัติการแก้ไข No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec time to disappear @@ -422,7 +400,8 @@ 1 day 1 วัน - time interval + delete after time +time interval 1 hour @@ -437,12 +416,26 @@ 1 month 1 เดือน - time interval + delete after time +time interval 1 week 1 สัปดาห์ - time interval + delete after time +time interval + + + 1 year + delete after time + + + 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 @@ -459,11 +452,6 @@ 30 วินาที No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -512,31 +500,29 @@ ยกเลิกการเปลี่ยนที่อยู่? No comment provided by engineer. - - About SimpleX - เกี่ยวกับ SimpleX - No comment provided by engineer. - About SimpleX Chat เกี่ยวกับ SimpleX Chat No comment provided by engineer. - - About SimpleX address - เกี่ยวกับที่อยู่ SimpleX + + About operators No comment provided by engineer. - - Accent color - สีเน้น + + Accent No comment provided by engineer. Accept รับ accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + No comment provided by engineer. Accept connection request? @@ -550,20 +536,40 @@ Accept incognito ยอมรับโหมดไม่ระบุตัวตน - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + No comment provided by engineer. + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active + token status text + + + 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 contact + + Add friends No comment provided by engineer. - - Add preset servers - เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า + + Add list No comment provided by engineer. @@ -571,14 +577,18 @@ เพิ่มโปรไฟล์ No comment provided by engineer. + + Add server + เพิ่มเซิร์ฟเวอร์ + No comment provided by engineer. + Add servers by scanning QR codes. เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด No comment provided by engineer. - - Add server… - เพิ่มเซิร์ฟเวอร์… + + Add team members No comment provided by engineer. @@ -586,11 +596,39 @@ เพิ่มเข้าไปในอุปกรณ์อื่น 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 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address ที่อยู่ @@ -601,6 +639,14 @@ การเปลี่ยนแปลงที่อยู่จะถูกยกเลิก จะใช้ที่อยู่เก่าของผู้รับ No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. No comment provided by engineer. @@ -615,6 +661,14 @@ การตั้งค่าระบบเครือข่ายขั้นสูง No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + + + All + No comment provided by engineer. + All app data is deleted. ข้อมูลแอปทั้งหมดถูกลบแล้ว. @@ -625,16 +679,28 @@ แชทและข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + alert message + 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. สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่. 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. @@ -648,6 +714,18 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + profile dropdown + + + 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. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. @@ -672,11 +750,19 @@ อนุญาตการโทรเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น. 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 downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น @@ -702,11 +788,19 @@ อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message) No comment provided by engineer. + + 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. + No comment provided by engineer. + Allow to send SimpleX links. No comment provided by engineer. @@ -769,6 +863,10 @@ Already joining the group! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay ใช้รีเลย์เสมอ @@ -779,11 +877,20 @@ โปรไฟล์แชทที่ว่างเปล่าพร้อมชื่อที่ให้ไว้ได้ถูกสร้างขึ้นและแอปจะเปิดตามปกติ No comment provided by engineer. + + Another reason + report reason + Answer call รับสาย No comment provided by engineer. + + Anybody can host servers. + โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้ + No comment provided by engineer. + App build: %@ รุ่นแอป: %@ @@ -797,6 +904,10 @@ App encrypts new local files (except videos). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon ไอคอนแอป @@ -812,6 +923,10 @@ รหัสผ่านแอปจะถูกแทนที่ด้วยรหัสผ่านที่ทำลายตัวเอง No comment provided by engineer. + + App session + No comment provided by engineer. + App version เวอร์ชันแอป @@ -831,10 +946,46 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + + + Archive + No comment provided by engineer. + + + Archive %lld reports? + 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? + No comment provided by engineer. + + + Archive reports + swipe action + + + Archived contacts + No comment provided by engineer. + Archiving database No comment provided by engineer. @@ -899,11 +1050,19 @@ ยอมรับภาพอัตโนมัติ No comment provided by engineer. + + Auto-accept settings + alert title + Back กลับ No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -918,15 +1077,51 @@ แฮชข้อความไม่ดี 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. @@ -955,6 +1150,14 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้ @@ -984,11 +1187,29 @@ Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + No comment provided by engineer. + + + 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: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! สิ้นสุดการโทรแล้ว! @@ -999,10 +1220,22 @@ โทร No comment provided by engineer. + + 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! ไม่สามารถเชิญผู้ติดต่อได้! @@ -1013,10 +1246,15 @@ ไม่สามารถเชิญผู้ติดต่อได้! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel ยกเลิก - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1027,10 +1265,18 @@ ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file ไม่สามารถรับไฟล์ได้ - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text Cellular @@ -1041,6 +1287,14 @@ เปลี่ยน No comment provided by engineer. + + Change automatic message deletion? + alert title + + + Change chat profiles + authentication reason + Change database passphrase? เปลี่ยนรหัสผ่านฐานข้อมูล? @@ -1085,11 +1339,22 @@ Change self-destruct passcode เปลี่ยนรหัสผ่านแบบทำลายตัวเอง authentication reason - set passcode view +set passcode view - - 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. @@ -1107,6 +1372,10 @@ ลบฐานข้อมูลแชทแล้ว No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported นำฐานข้อมูลแชทเข้าแล้ว @@ -1126,6 +1395,10 @@ 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. @@ -1135,15 +1408,44 @@ ค่ากําหนดในการแชท No comment provided by engineer. + + Chat preferences were changed. + alert message + + + 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. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1164,10 +1466,22 @@ เลือกจากอัลบั้ม No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear ลบ - No comment provided by engineer. + swipe action Clear conversation @@ -1179,6 +1493,14 @@ ลบการสนทนา? 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. @@ -1188,11 +1510,18 @@ ล้างการยืนยัน No comment provided by engineer. - - Colors - สี + + Color chats with the new themes. No comment provided by engineer. + + Color mode + No comment provided by engineer. + + + Community guidelines violation + report reason + Compare file เปรียบเทียบไฟล์ @@ -1203,11 +1532,47 @@ เปรียบเทียบรหัสความปลอดภัยกับผู้ติดต่อของคุณ 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 ยืนยัน @@ -1218,11 +1583,19 @@ ยืนยันรหัสผ่าน 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. @@ -1245,6 +1618,10 @@ Confirm upload No comment provided by engineer. + + Confirmed + token status text + Connect เชื่อมต่อ @@ -1262,6 +1639,10 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? No comment provided by engineer. @@ -1293,14 +1674,26 @@ This is your own one-time link! Connect with %@ 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 to server… กำลังเชื่อมต่อกับเซิร์ฟเวอร์… @@ -1311,6 +1704,10 @@ This is your own one-time link! กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@) 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. @@ -1320,6 +1717,14 @@ This is your own one-time link! การเชื่อมต่อ No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + + + Connection blocked + No comment provided by engineer. + Connection error การเชื่อมต่อผิดพลาด @@ -1330,11 +1735,32 @@ This is your own one-time link! การเชื่อมต่อผิดพลาด (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + 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. @@ -1344,6 +1770,14 @@ This is your own one-time link! หมดเวลาการเชื่อมต่อ No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows ผู้ติดต่ออนุญาต @@ -1354,6 +1788,10 @@ This is your own one-time link! ผู้ติดต่อรายนี้มีอยู่แล้ว No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: ผู้ติดต่อถูกซ่อน: @@ -1364,9 +1802,8 @@ This is your own one-time link! เชื่อมต่อกับผู้ติดต่อแล้ว notification - - Contact is not connected yet! - ผู้ติดต่อยังไม่ได้เชื่อมต่อ! + + Contact is deleted. No comment provided by engineer. @@ -1379,6 +1816,10 @@ This is your own one-time link! การกําหนดลักษณะการติดต่อ No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts ติดต่อ @@ -1389,21 +1830,37 @@ This is your own one-time link! ผู้ติดต่อสามารถทําเครื่องหมายข้อความเพื่อลบได้ คุณจะสามารถดูได้ No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue ดำเนินการต่อ No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy คัดลอก - chat item action + 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. @@ -1413,6 +1870,10 @@ This is your own one-time link! สร้าง No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address สร้างที่อยู่ SimpleX @@ -1422,11 +1883,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ - No comment provided by engineer. - Create file สร้างไฟล์ @@ -1446,6 +1902,10 @@ This is your own one-time link! สร้างลิงค์ No comment provided by engineer. + + Create list + No comment provided by engineer. + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 No comment provided by engineer. @@ -1469,6 +1929,10 @@ This is your own one-time link! สร้างโปรไฟล์ของคุณ No comment provided by engineer. + + Created + No comment provided by engineer. + Created at No comment provided by engineer. @@ -1477,11 +1941,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - สร้างเมื่อ %@ - No comment provided by engineer. - Creating archive link No comment provided by engineer. @@ -1495,11 +1954,19 @@ This is your own one-time link! รหัสผ่านปัจจุบัน 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 passphrase… รหัสผ่านปัจจุบัน… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. ขนาดไฟล์ที่รองรับสูงสุดในปัจจุบันคือ %@ @@ -1510,11 +1977,23 @@ This is your own one-time link! เวลาที่กําหนดเอง 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 ID ID ฐานข้อมูล @@ -1613,6 +2092,10 @@ This is your own one-time link! ระบบจะย้ายฐานข้อมูลเมื่อแอปรีสตาร์ท No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized กระจายอำนาจแล้ว @@ -1626,17 +2109,17 @@ This is your own one-time link! Delete ลบ - chat item action + alert action +swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - ลบผู้ติดต่อ - No comment provided by engineer. - Delete address ลบที่อยู่ @@ -1661,14 +2144,12 @@ This is your own one-time link! 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. @@ -1681,6 +2162,10 @@ This is your own one-time link! ลบโปรไฟล์แชทไหม? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection ลบการเชื่อมต่อ @@ -1691,9 +2176,8 @@ This is your own one-time link! ลบผู้ติดต่อ No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1755,6 +2239,10 @@ This cannot be undone! ลบลิงค์ ไหม? No comment provided by engineer. + + Delete list? + alert title + Delete member message? ลบข้อความสมาชิก? @@ -1768,7 +2256,7 @@ This cannot be undone! Delete messages ลบข้อความ - No comment provided by engineer. + alert button Delete messages after @@ -1785,9 +2273,8 @@ This cannot be undone! ลบฐานข้อมูลเก่า? No comment provided by engineer. - - Delete pending connection - ลบการเชื่อมต่อที่รอดำเนินการ + + Delete or moderate up to 200 messages. No comment provided by engineer. @@ -1805,11 +2292,27 @@ This cannot be undone! ลบคิว server test step + + Delete report + No comment provided by engineer. + + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? ลบโปรไฟล์ผู้ใช้? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at ลบที่ @@ -1820,6 +2323,14 @@ This cannot be undone! ลบที่: %@ copied message info + + Deletion errors + No comment provided by engineer. + + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery No comment provided by engineer. @@ -1851,11 +2362,35 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + 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 เครื่องมือสำหรับนักพัฒนา @@ -1886,8 +2421,12 @@ This cannot be undone! ข้อความโดยตรง chat feature - - 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. @@ -1901,11 +2440,23 @@ This cannot be undone! ปิดการใช้งาน SimpleX Lock authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all ปิดการใช้งานสำหรับทุกคน No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) @@ -1921,8 +2472,8 @@ This cannot be undone! ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในแชทนี้ No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. @@ -1953,11 +2504,19 @@ This cannot be undone! Discover via local network 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 SimpleX for emergency calls. อย่าใช้ SimpleX สําหรับการโทรฉุกเฉิน No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later ทำในภายหลัง @@ -1967,6 +2526,14 @@ This cannot be undone! Do not send history to new members. No comment provided by engineer. + + Do not use credentials with proxy. + No comment provided by engineer. + + + Documents: + No comment provided by engineer. + Don't create address อย่าสร้างที่อยู่ @@ -1977,11 +2544,19 @@ This cannot be undone! อย่าเปิดใช้งาน 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 ปรับลดรุ่นและเปิดแชท @@ -1989,7 +2564,12 @@ This cannot be undone! Download - chat item action + alert button +chat item action + + + Download errors + No comment provided by engineer. Download failed @@ -2000,6 +2580,18 @@ This cannot be undone! ดาวน์โหลดไฟล์ server test step + + Download files + alert action + + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive No comment provided by engineer. @@ -2018,6 +2610,10 @@ This cannot be undone! ระยะเวลา No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit แก้ไข @@ -2038,6 +2634,10 @@ This cannot be undone! เปิดใช้งาน (เก็บการแทนที่) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock เปิดใช้งาน SimpleX Lock @@ -2051,7 +2651,7 @@ This cannot be undone! Enable automatic message deletion? เปิดใช้งานการลบข้อความอัตโนมัติ? - No comment provided by engineer. + alert title Enable camera access @@ -2096,6 +2696,10 @@ This cannot be undone! เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2165,6 +2769,10 @@ This cannot be undone! Encryption re-negotiation failed. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode ใส่รหัสผ่าน @@ -2226,26 +2834,33 @@ This cannot be undone! เกิดข้อผิดพลาดในการยกเลิกการเปลี่ยนที่อยู่ No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request เกิดข้อผิดพลาดในการรับคำขอติดต่อ No comment provided by engineer. - - Error accessing database file - เกิดข้อผิดพลาดในการเข้าถึงไฟล์ฐานข้อมูล - No comment provided by engineer. - Error adding member(s) เกิดข้อผิดพลาดในการเพิ่มสมาชิก No comment provided by engineer. + + Error adding server + alert title + Error changing address เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่ No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role เกิดข้อผิดพลาดในการเปลี่ยนบทบาท @@ -2256,6 +2871,18 @@ This cannot be undone! เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า 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 เกิดข้อผิดพลาดในการสร้างที่อยู่ @@ -2271,6 +2898,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการสร้างลิงก์กลุ่ม No comment provided by engineer. + + Error creating list + alert title + Error creating member contact No comment provided by engineer. @@ -2284,6 +2915,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการสร้างโปรไฟล์! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file No comment provided by engineer. @@ -2303,11 +2938,6 @@ This cannot be undone! เกิดข้อผิดพลาดในการลบการเชื่อมต่อ No comment provided by engineer. - - Error deleting contact - เกิดข้อผิดพลาดในการลบผู้ติดต่อ - No comment provided by engineer. - Error deleting database เกิดข้อผิดพลาดในการลบฐานข้อมูล @@ -2352,6 +2982,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท @@ -2362,9 +2996,12 @@ This cannot be undone! เกิดข้อผิดพลาดในการเข้าร่วมกลุ่ม No comment provided by engineer. - - Error loading %@ servers - โหลดเซิร์ฟเวอร์ %@ ผิดพลาด + + Error loading servers + alert title + + + Error migrating settings No comment provided by engineer. @@ -2374,16 +3011,31 @@ This cannot be undone! Error receiving file เกิดข้อผิดพลาดในการรับไฟล์ + alert title + + + Error reconnecting server No comment provided by engineer. + + Error reconnecting servers + No comment provided by engineer. + + + Error registering for notifications + alert title + Error removing member เกิดข้อผิดพลาดในการลบสมาชิก No comment provided by engineer. - - Error saving %@ servers - เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@ + + Error reordering lists + alert title + + + Error resetting statistics No comment provided by engineer. @@ -2391,6 +3043,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม @@ -2406,6 +3062,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการบันทึกรหัสผ่านไปยัง keychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings when migrating @@ -2448,16 +3108,24 @@ This cannot be undone! เกิดข้อผิดพลาดในการหยุดแชท No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์! - No comment provided by engineer. + alertTitle Error synchronizing connection เกิดข้อผิดพลาดในการซิงโครไนซ์การเชื่อมต่อ No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link เกิดข้อผิดพลาดในการอัปเดตลิงก์กลุ่ม @@ -2468,6 +3136,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการอัปเดตข้อความ No comment provided by engineer. + + Error updating server + alert title + Error updating settings เกิดข้อผิดพลาดในการอัปเดตการตั้งค่า @@ -2494,7 +3166,9 @@ This cannot be undone! Error: %@ ข้อผิดพลาด: % @ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2506,6 +3180,14 @@ This cannot be undone! เกิดข้อผิดพลาด: ไม่มีแฟ้มฐานข้อมูล No comment provided by engineer. + + Errors + No comment provided by engineer. + + + Errors in servers configuration. + servers error + Even when disabled in the conversation. แม้ในขณะที่ปิดใช้งานในการสนทนา @@ -2520,6 +3202,10 @@ This cannot be undone! Expand chat item action + + Expired + token status text + Export database ส่งออกฐานข้อมูล @@ -2530,6 +3216,10 @@ This cannot be undone! ข้อผิดพลาดในการส่งออก: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. ที่เก็บถาวรฐานข้อมูลที่ส่งออก @@ -2554,15 +3244,57 @@ This cannot be undone! รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์! 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. + No comment provided by engineer. + Favorite ที่ชอบ + swipe action + + + Favorites No comment provided by engineer. + + File error + file error alert title + + + File errors: +%@ + alert message + + + File is blocked by server operator: +%@. + file error text + + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. ไฟล์จะถูกลบออกจากเซิร์ฟเวอร์ @@ -2583,6 +3315,10 @@ This cannot be undone! ไฟล์: % @ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media ไฟล์และสื่อ @@ -2593,8 +3329,8 @@ This cannot be undone! ไฟล์และสื่อ chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. @@ -2660,19 +3396,59 @@ This cannot be undone! การแก้ไขไม่สนับสนุนโดยสมาชิกกลุ่ม No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + servers error + 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. + No comment provided by engineer. + + + For me + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward chat item action + + Forward %d message(s)? + alert title + Forward and save messages No comment provided by engineer. + + Forward messages + alert action + + + Forward messages without files? + alert message + + + Forward up to 20 messages at once. + No comment provided by engineer. + Forwarded No comment provided by engineer. @@ -2681,6 +3457,32 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding %lld messages + 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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop No comment provided by engineer. @@ -2700,11 +3502,6 @@ This cannot be undone! ชื่อเต็ม (ไม่บังคับ) No comment provided by engineer. - - Full name: - ชื่อเต็ม: - No comment provided by engineer. - Fully decentralized – visible only to members. No comment provided by engineer. @@ -2724,6 +3521,18 @@ This cannot be undone! GIFs และสติกเกอร์ No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + message preview + + + Good morning! + message preview + Group กลุ่ม @@ -2777,40 +3586,6 @@ This cannot be undone! ลิงค์กลุ่ม No comment provided by engineer. - - Group members can add message reactions. - สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. (24 hours) - สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร - No comment provided by engineer. - - - Group members can send SimpleX links. - 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. - สมาชิกกลุ่มสามารถส่งข้อความเสียง - No comment provided by engineer. - Group message: ข้อความกลุ่ม: @@ -2851,11 +3626,19 @@ 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 ซ่อนอยู่ @@ -2905,10 +3688,17 @@ This cannot be undone! วิธีการ SimpleX ทํางานอย่างไร No comment provided by engineer. + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy + No comment provided by engineer. + How it works - มันทำงานอย่างไร - No comment provided by engineer. + alert button How to @@ -2934,6 +3724,10 @@ This cannot be undone! เซิร์ฟเวอร์ ICE (หนึ่งเครื่องต่อสาย) No comment provided by engineer. + + IP address + No comment provided by engineer. + If you can't meet in person, show QR code in a video call, or share the link. หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์ @@ -2974,8 +3768,8 @@ This cannot be undone! โดยทันที No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam มีภูมิคุ้มกันต่อสแปมและการละเมิด No comment provided by engineer. @@ -2998,10 +3792,19 @@ This cannot be undone! 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. +More improvements are coming soon! + No comment provided by engineer. + Improved message delivery No comment provided by engineer. @@ -3029,6 +3832,14 @@ This cannot be undone! In-call sounds No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito ไม่ระบุตัวตน @@ -3096,6 +3907,11 @@ This cannot be undone! ติดตั้ง [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! @@ -3103,16 +3919,35 @@ This cannot be undone! No comment provided by engineer. - - Instantly - ทันที - No comment provided by engineer. - Interface อินเตอร์เฟซ No comment provided by engineer. + + Interface colors + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code No comment provided by engineer. @@ -3145,7 +3980,7 @@ This cannot be undone! Invalid server address! ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง! - No comment provided by engineer. + alert title Invalid status @@ -3166,6 +4001,10 @@ This cannot be undone! เชิญสมาชิก No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group เชิญเข้าร่วมกลุ่ม @@ -3181,8 +4020,8 @@ This cannot be undone! ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้ No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้ No comment provided by engineer. @@ -3207,6 +4046,10 @@ This cannot be undone! 3. การเชื่อมต่อถูกบุกรุก No comment provided by engineer. + + It protects your IP address and connections. + 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 (%@). ดูเหมือนว่าคุณได้เชื่อมต่อผ่านลิงก์นี้แล้ว หากไม่เป็นเช่นนั้น แสดงว่ามีข้อผิดพลาด (%@). @@ -3225,7 +4068,7 @@ This cannot be undone! Join เข้าร่วม - No comment provided by engineer. + swipe action Join group @@ -3261,6 +4104,10 @@ This is your link for group %@! Keep + alert action + + + Keep conversation No comment provided by engineer. @@ -3269,7 +4116,7 @@ This is your link for group %@! Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3304,6 +4151,14 @@ This is your link for group %@! Leave ออกจาก + swipe action + + + Leave chat + No comment provided by engineer. + + + Leave chat? No comment provided by engineer. @@ -3343,6 +4198,18 @@ This is your link for group %@! Linked desktops No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! ข้อความสด! @@ -3353,11 +4220,6 @@ This is your link for group %@! ข้อความสด No comment provided by engineer. - - Local - ในเครื่อง - No comment provided by engineer. - Local name ชื่อภายในเครื่องเท่านั้น @@ -3378,11 +4240,6 @@ This is your link for group %@! โหมดล็อค No comment provided by engineer. - - Make a private connection - สร้างการเชื่อมต่อแบบส่วนตัว - No comment provided by engineer. - Make one message disappear ทำให้ข้อความหายไปหนึ่งข้อความ @@ -3393,21 +4250,11 @@ This is your link for group %@! ทำให้โปรไฟล์เป็นส่วนตัว! 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 ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน @@ -3433,11 +4280,31 @@ This is your link for group %@! สูงสุด 30 วินาที รับทันที No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member สมาชิก No comment provided by engineer. + + Member inactive + item status text + + + Member reports + chat feature + + + 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. บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง @@ -3448,11 +4315,61 @@ This is your link for group %@! บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกจะได้รับคำเชิญใหม่ 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. + No comment provided by engineer. + + + Members can send SimpleX links. + 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. + สมาชิกกลุ่มสามารถส่งข้อความเสียง + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + No comment provided by engineer. + Message delivery error ข้อผิดพลาดในการส่งข้อความ @@ -3463,11 +4380,27 @@ This is your link for group %@! ใบเสร็จการส่งข้อความ! No comment provided by engineer. + + Message delivery warning + item status text + Message draft ร่างข้อความ No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions ปฏิกิริยาของข้อความ @@ -3478,15 +4411,35 @@ This is your link for group %@! ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้ No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้ 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 + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text ข้อความ @@ -3510,6 +4463,22 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + + + Messages were deleted after you selected them. + alert message + 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. @@ -3566,9 +4535,9 @@ This is your link for group %@! การโยกย้ายเสร็จสมบูรณ์ No comment provided by engineer. - - Migrations: %@ - การย้ายข้อมูล: %@ + + Migrations: + การย้ายข้อมูล No comment provided by engineer. @@ -3586,6 +4555,10 @@ This is your link for group %@! กลั่นกรองที่: %@ copied message info + + More + swipe action + More improvements are coming soon! การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้! @@ -3595,15 +4568,14 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. item status description - - Most likely this contact has deleted the connection with you. - เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ - No comment provided by engineer. - Multiple chat profiles โปรไฟล์การแชทหลายรายการ @@ -3612,7 +4584,11 @@ This is your link for group %@! Mute ปิดเสียง - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3622,7 +4598,7 @@ This is your link for group %@! Name ชื่อ - No comment provided by engineer. + swipe action Network & servers @@ -3633,10 +4609,22 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + snd error text + Network management No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings การตั้งค่าเครือข่าย @@ -3647,15 +4635,31 @@ This is your link for group %@! สถานะเครือข่าย No comment provided by engineer. + + New + token status text + New Passcode รหัสผ่านใหม่ No comment provided by engineer. + + New SOCKS credentials will be used every time you start the app. + No comment provided by engineer. + + + New SOCKS credentials will be used for each server. + No comment provided by engineer. + New chat No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request คำขอติดต่อใหม่ @@ -3666,11 +4670,6 @@ This is your link for group %@! คำขอติดต่อใหม่: notification - - New database archive - ฐานข้อมูลใหม่สำหรับการเก็บถาวร - No comment provided by engineer. - New desktop app! No comment provided by engineer. @@ -3680,11 +4679,19 @@ This is your link for group %@! ชื่อที่แสดงใหม่ No comment provided by engineer. + + New events + notification + New in %@ ใหม่ใน %@ No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role บทบาทของสมาชิกใหม่ @@ -3700,6 +4707,10 @@ This is your link for group %@! รหัสผ่านใหม่… No comment provided by engineer. + + New server + No comment provided by engineer. + No เลขที่ @@ -3710,6 +4721,18 @@ This is your link for group %@! ไม่มีรหัสผ่านสำหรับแอป Authentication unavailable + + 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 ไม่ได้เลือกผู้ติดต่อ @@ -3729,6 +4752,10 @@ This is your link for group %@! ไม่มีโทเค็นอุปกรณ์! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats ไม่มีการกรองการแชท @@ -3744,24 +4771,94 @@ This is your link for group %@! ไม่มีประวัติ No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + + + No media & file servers. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + servers error + 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 ไม่มีไฟล์ที่ได้รับหรือส่ง No comment provided by engineer. + + 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. + servers error + + + No token! + alert title + + + 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 + No comment provided by engineer. + + + Nothing to forward! + alert title + Notifications การแจ้งเตือน @@ -3772,6 +4869,18 @@ This is your link for group %@! ปิดการแจ้งเตือน! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3788,35 +4897,32 @@ This is your link for group %@! Off ปิด - No comment provided by engineer. + blur media Ok ตกลง - No comment provided by engineer. + alert button Old database ฐานข้อมูลเก่า No comment provided by engineer. - - Old database archive - คลังฐานข้อมูลเก่า - 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 hosts will be **required** for connection. +Requires compatible VPN. จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. + + Onion hosts will be used when available. +Requires compatible VPN. จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ No comment provided by engineer. @@ -3825,11 +4931,19 @@ This is your link for group %@! โฮสต์หัวหอมจะไม่ถูกใช้ 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. เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น** No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้ @@ -3845,6 +4959,14 @@ This is your link for group %@! เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานข้อความเสียงได้ 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. มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้ @@ -3897,13 +5019,17 @@ This is your link for group %@! Open - No comment provided by engineer. + alert action Open Settings เปิดการตั้งค่า No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat เปิดแชท @@ -3914,28 +5040,38 @@ This is your link for group %@! เปิดคอนโซลการแชท authentication reason + + Open conditions + No comment provided by engineer. + Open group No comment provided by engineer. + + Open link? + alert title + Open migration to another device authentication reason - - Open user profiles - เปิดโปรไฟล์ผู้ใช้ - authentication reason - - - Open-source protocol and code – anybody can run the servers. - โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้ - No comment provided by engineer. - Opening app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + + + Or import archive file + No comment provided by engineer. + Or paste archive link No comment provided by engineer. @@ -3952,10 +5088,23 @@ This is your link for group %@! 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 file errors: +%@ + alert message + PING count จํานวน PING @@ -3991,6 +5140,10 @@ This is your link for group %@! ตั้งรหัสผ่านเรียบร้อยแล้ว! No comment provided by engineer. + + Password + No comment provided by engineer. + Password to show รหัสผ่านที่จะแสดง @@ -4017,13 +5170,12 @@ This is your link for group %@! Paste the link you received No comment provided by engineer. - - People can connect to you only via the links you share. - ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น + + Pending No comment provided by engineer. - - Periodically + + Periodic เป็นระยะๆ No comment provided by engineer. @@ -4036,11 +5188,24 @@ This is your link for group %@! Picture-in-picture calls 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. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. โปรดตรวจสอบว่าคุณใช้ลิงก์ที่ถูกต้องหรือขอให้ผู้ติดต่อของคุณส่งลิงก์ใหม่ให้คุณ @@ -4105,11 +5270,27 @@ Error: %@ โปรดจัดเก็บรหัสผ่านอย่างปลอดภัย คุณจะไม่สามารถเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย No comment provided by engineer. + + 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. + token info + Polish interface อินเตอร์เฟซภาษาโปแลนด์ No comment provided by engineer. + + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง @@ -4120,40 +5301,75 @@ Error: %@ เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ No comment provided by engineer. - - Preset server - เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า - No comment provided by engineer. - Preset server address ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า No comment provided by engineer. + + 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 🚀 + No comment provided by engineer. + Private notes name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์ @@ -4168,23 +5384,19 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - No comment provided by engineer. - Profile password รหัสผ่านโปรไฟล์ No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4206,6 +5418,10 @@ Error: %@ ห้ามแสดงปฏิกิริยาต่อข้อความ No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. No comment provided by engineer. @@ -4230,11 +5446,20 @@ Error: %@ ห้ามส่งข้อความเสียง No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen ปกป้องหน้าจอแอป No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน! @@ -4250,6 +5475,18 @@ Error: %@ การหมดเวลาของโปรโตคอลต่อ 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 การแจ้งเตือนแบบทันที @@ -4268,6 +5505,10 @@ Error: %@ ให้คะแนนแอป No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… ตอบสนอง… @@ -4276,32 +5517,27 @@ Error: %@ Read อ่าน - No comment provided by engineer. + swipe action Read more อ่านเพิ่มเติม No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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) No comment provided by engineer. - - Read more in our GitHub repository. - อ่านเพิ่มเติมในที่เก็บ GitHub ของเรา - 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) @@ -4311,6 +5547,10 @@ Error: %@ Receipts are disabled No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at ได้รับเมื่อ @@ -4331,15 +5571,23 @@ Error: %@ ได้รับข้อความ message info title + + 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. ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์ No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. การรับไฟล์จะหยุดลง @@ -4363,11 +5611,31 @@ Error: %@ ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์ 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? เชื่อมต่อเซิร์ฟเวอร์อีกครั้งหรือไม่? @@ -4388,10 +5656,23 @@ Error: %@ ลดการใช้แบตเตอรี่ No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject ปฏิเสธ - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4417,6 +5698,14 @@ Error: %@ ลบ No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + + + Remove image + No comment provided by engineer. + Remove member ลบสมาชิกออก @@ -4472,6 +5761,46 @@ Error: %@ ตอบ chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required ที่จำเป็น @@ -4482,16 +5811,36 @@ Error: %@ รีเซ็ต 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 รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่ @@ -4531,9 +5880,8 @@ Error: %@ เปิดเผย chat item action - - Revert - เปลี่ยนกลับ + + Review conditions No comment provided by engineer. @@ -4561,9 +5909,16 @@ Error: %@ เรียกใช้แชท No comment provided by engineer. - - SMP servers - เซิร์ฟเวอร์ SMP + + SMP server + No comment provided by engineer. + + + SOCKS proxy + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4573,43 +5928,42 @@ Error: %@ Save บันทึก - chat item action + alert button +chat item action Save (and notify contacts) บันทึก (และแจ้งผู้ติดต่อ) - No comment provided by engineer. + alert button Save and notify contact บันทึกและแจ้งผู้ติดต่อ - No comment provided by engineer. + alert button 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 บันทึกรหัสผ่านและเปิดแชท @@ -4623,7 +5977,7 @@ Error: %@ Save preferences? บันทึกการตั้งค่า? - No comment provided by engineer. + alert title Save profile password @@ -4638,18 +5992,17 @@ Error: %@ Save servers? บันทึกเซิร์ฟเวอร์? - No comment provided by engineer. - - - Save settings? - บันทึกการตั้งค่า? - No comment provided by engineer. + alert title Save welcome message? บันทึกข้อความต้อนรับ? No comment provided by engineer. + + Save your profile? + alert title + Saved No comment provided by engineer. @@ -4667,6 +6020,18 @@ Error: %@ Saved message message info title + + Saving %lld messages + No comment provided by engineer. + + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code สแกนคิวอาร์โค้ด @@ -4704,11 +6069,19 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue คิวที่ปลอดภัย server test step + + Secured + No comment provided by engineer. + Security assessment การประเมินความปลอดภัย @@ -4722,6 +6095,18 @@ Error: %@ Select เลือก + chat item action + + + Select chat profile + No comment provided by engineer. + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4759,11 +6144,6 @@ Error: %@ ส่งใบเสร็จรับการจัดส่งข้อความไปที่ No comment provided by engineer. - - Send direct message - ส่งข้อความโดยตรง - No comment provided by engineer. - Send direct message to connect No comment provided by engineer. @@ -4773,6 +6153,10 @@ Error: %@ ส่งข้อความแบบที่หายไป No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews ส่งตัวอย่างลิงก์ @@ -4783,14 +6167,25 @@ Error: %@ ส่งข้อความสด 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. + 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. @@ -4815,7 +6210,7 @@ Error: %@ Sender cancelled file transfer. ผู้ส่งยกเลิกการโอนไฟล์ - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -4870,6 +6265,10 @@ Error: %@ ส่งเมื่อ: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event เหตุการณ์ไฟล์ที่ส่ง @@ -4880,11 +6279,59 @@ Error: %@ ข้อความที่ส่งแล้ว message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + alert message + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน @@ -4900,11 +6347,31 @@ Error: %@ การทดสอบเซิร์ฟเวอร์ล้มเหลว! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + 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. @@ -4914,11 +6381,19 @@ Error: %@ ตั้ง 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 ตั้งค่าการกําหนดลักษณะกลุ่ม @@ -4929,6 +6404,10 @@ Error: %@ ตั้งแทนการรับรองความถูกต้องของระบบ No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode ตั้งรหัสผ่าน @@ -4958,6 +6437,10 @@ Error: %@ การตั้งค่า No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images No comment provided by engineer. @@ -4965,21 +6448,38 @@ Error: %@ Share แชร์ - chat item action + alert action +chat item action Share 1-time link แชร์ลิงก์แบบใช้ครั้งเดียว No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address แชร์ที่อยู่ No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? แชร์ที่อยู่กับผู้ติดต่อ? + alert title + + + Share from other apps. No comment provided by engineer. @@ -4987,15 +6487,27 @@ Error: %@ แชร์ลิงก์ No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts แชร์กับผู้ติดต่อ No comment provided by engineer. + + Short link + No comment provided by engineer. + Show QR code No comment provided by engineer. @@ -5014,21 +6526,41 @@ Error: %@ 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 → on messages sent via private routing. + No comment provided by engineer. + Show: แสดง: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address ที่อยู่ SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. ความปลอดภัยของ SimpleX Chat ได้รับการตรวจสอบโดย Trail of Bits @@ -5059,6 +6591,18 @@ Error: %@ ที่อยู่ SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + + + SimpleX channel link + simplex link type + SimpleX contact address ที่อยู่ติดต่อ SimpleX @@ -5079,8 +6623,8 @@ Error: %@ ลิงก์ SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. No comment provided by engineer. @@ -5092,10 +6636,18 @@ Error: %@ คำเชิญ SimpleX แบบครั้งเดียว simplex link type + + SimpleX protocols reviewed by Trail of Bits. + No comment provided by engineer. + Simplified incognito mode No comment provided by engineer. + + Size + No comment provided by engineer. + Skip ข้าม @@ -5110,16 +6662,42 @@ Error: %@ Small groups (max 20) No comment provided by engineer. + + Soft + blur media + + + 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. ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + + + Some servers failed the test: +%@ + alert message + Somebody ใครบางคน notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. No comment provided by engineer. @@ -5138,6 +6716,14 @@ Error: %@ เริ่มการย้ายข้อมูล No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop หยุด @@ -5152,11 +6738,6 @@ Error: %@ Stop chat 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. หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง @@ -5185,27 +6766,55 @@ Error: %@ Stop sharing หยุดแชร์ - No comment provided by engineer. + alert action Stop sharing address? หยุดแชร์ที่อยู่ไหม? - No comment provided by engineer. + alert title Stopping chat No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + blur media + 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 ระบบ @@ -5216,11 +6825,19 @@ Error: %@ การรับรองความถูกต้องของระบบ No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout หมดเวลาการเชื่อมต่อ TCP No comment provided by engineer. + + TCP port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5236,11 +6853,19 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture ถ่ายภาพ No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button แตะปุ่ม @@ -5273,16 +6898,19 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - แตะเพื่อเริ่มแชทใหม่ - No comment provided by engineer. + + Temporary file error + file error alert title Test failed at step %@. การทดสอบล้มเหลวในขั้นตอน %@ server test failure + + Test notifications + No comment provided by engineer. + Test server เซิร์ฟเวอร์ทดสอบ @@ -5296,7 +6924,7 @@ Error: %@ Tests failed! การทดสอบล้มเหลว! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5313,11 +6941,6 @@ Error: %@ ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว - No comment provided by engineer. - 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. @@ -5331,6 +6954,14 @@ It can happen because of some bug or when the connection is compromised.แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน 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). + No comment provided by engineer. + The attempt to change database passphrase was not completed. ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์ @@ -5340,6 +6971,10 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก! @@ -5360,6 +6995,11 @@ It can happen because of some bug or when the connection is compromised.encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้! No comment provided by engineer. + + The future of messaging + การส่งข้อความส่วนตัวรุ่นต่อไป + No comment provided by engineer. + The hash of the previous message is different. แฮชของข้อความก่อนหน้านี้แตกต่างกัน @@ -5375,9 +7015,12 @@ It can happen because of some bug or when the connection is compromised.ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน 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. @@ -5385,9 +7028,12 @@ It can happen because of some bug or when the connection is compromised.ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้ No comment provided by engineer. - - The profile is only shared with your contacts. - โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! No comment provided by engineer. @@ -5405,13 +7051,24 @@ It can happen because of some bug or when the connection is compromised.เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@** No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - ธีม + + 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. @@ -5433,6 +7090,10 @@ It can happen because of some bug or when the connection is compromised.การดำเนินการนี้ไม่สามารถเลิกทำได้ - ข้อความที่ส่งและรับก่อนหน้าที่เลือกไว้จะถูกลบ อาจใช้เวลาหลายนาที No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร @@ -5471,11 +7132,27 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! 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. + 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: หากต้องการถามคำถามและรับการอัปเดต: @@ -5495,9 +7172,8 @@ It can happen because of some bug or when the connection is compromised.เพื่อสร้างการเชื่อมต่อใหม่ 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 มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -5505,6 +7181,10 @@ It can happen because of some bug or when the connection is compromised.ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5512,6 +7192,23 @@ You will be prompted to complete authentication before this feature is enabled.< คุณจะได้รับแจ้งให้ยืนยันตัวตนให้เสร็จสมบูรณ์ก่อนที่จะเปิดใช้งานคุณลักษณะนี้ 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 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. ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน @@ -5522,25 +7219,53 @@ You will be prompted to complete authentication before this feature is enabled.< หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้า **โปรไฟล์แชทของคุณ** 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. ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. + + Token status: %@. + token status + + + 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: %@). กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %@) @@ -5590,10 +7315,9 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. - - Unexpected error: %@ - ข้อผิดพลาดที่ไม่คาดคิด: %@ - item status description + + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5603,7 +7327,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. เลิกชอบ - No comment provided by engineer. + swipe action Unhide @@ -5640,6 +7364,10 @@ You will be prompted to complete authentication before this feature is enabled.< ข้อผิดพลาดที่ไม่รู้จัก No comment provided by engineer. + + Unknown servers! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. ยกเว้นกรณีที่คุณใช้อินเทอร์เฟซการโทรของ iOS ให้เปิดใช้งานโหมดห้ามรบกวนเพื่อหลีกเลี่ยงการรบกวน @@ -5673,11 +7401,15 @@ To connect, please ask your contact to create another connection link and check Unmute เปิดเสียง - No comment provided by engineer. + notification label action Unread เปลี่ยนเป็นยังไม่ได้อ่าน + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5689,11 +7421,6 @@ To connect, please ask your contact to create another connection link and check อัปเดต No comment provided by engineer. - - Update .onion hosts setting? - อัปเดตการตั้งค่าโฮสต์ .onion ไหม? - No comment provided by engineer. - Update database passphrase อัปเดตรหัสผ่านของฐานข้อมูล @@ -5704,9 +7431,12 @@ To connect, please ask your contact to create another connection link and check อัปเดตการตั้งค่าเครือข่ายไหม? No comment provided by engineer. - - Update transport isolation mode? - อัปเดตโหมดการแยกการขนส่งไหม? + + Update settings? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5714,16 +7444,15 @@ To connect, please ask your contact to create another connection link and check การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง 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 อัปเกรดและเปิดการแชท No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5733,20 +7462,44 @@ To connect, please ask your contact to create another connection link and check อัปโหลดไฟล์ server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts ใช้โฮสต์ .onion No comment provided by engineer. + + Use SOCKS proxy + No comment provided by engineer. + Use SimpleX Chat servers? ใช้เซิร์ฟเวอร์ SimpleX Chat ไหม? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat ใช้แชท @@ -5756,6 +7509,14 @@ To connect, please ask your contact to create another connection link and check 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 ใช้สำหรับการเชื่อมต่อใหม่ @@ -5778,23 +7539,45 @@ To connect, please ask your contact to create another connection link and check Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + 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 short links (BETA) + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. - - User profile - โปรไฟล์ผู้ใช้ + + Use the app with one hand. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้ + + Use web port + No comment provided by engineer. + + + User selection + No comment provided by engineer. + + + Username No comment provided by engineer. @@ -5861,11 +7644,19 @@ To connect, please ask your contact to create another connection link and check วิดีโอและไฟล์สูงสุด 1gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code ดูรหัสความปลอดภัย No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history chat feature @@ -5880,8 +7671,8 @@ To connect, please ask your contact to create another connection link and check ห้ามส่งข้อความเสียงในแชทนี้ No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. @@ -5918,6 +7709,14 @@ To connect, please ask your contact to create another connection link and check กําลังรอวิดีโอ No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + 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. @@ -5960,9 +7759,8 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้ + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -5995,24 +7793,34 @@ To connect, please ask your contact to create another connection link and check With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + alert message + Wrong database passphrase รหัสผ่านฐานข้อมูลไม่ถูกต้อง No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! รหัสผ่านผิด! No comment provided by engineer. - - XFTP servers - เซิร์ฟเวอร์ XFTP - No comment provided by engineer. - - - You - คุณ + + XFTP server No comment provided by engineer. @@ -6039,6 +7847,10 @@ To connect, please ask your contact to create another connection link and check คุณได้เชื่อมต่อกับ %@ แล้ว 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. @@ -6078,11 +7890,23 @@ Repeat join request? คุณได้รับเชิญให้เข้าร่วมกลุ่ม 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 can accept calls from lock screen, without device and app authentication. คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป No comment provided by engineer. + + 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 คุณสามารถสร้างได้ในภายหลัง @@ -6111,11 +7935,19 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ ตอนนี้คุณสามารถส่งข้อความถึง %@ notification body + + 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. คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า @@ -6131,16 +7963,15 @@ Repeat join request? คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ **%@** No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้ - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่า @@ -6153,23 +7984,23 @@ Repeat join request? You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! คุณไม่สามารถส่งข้อความได้! 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 decide who can connect. + ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น + No comment provided by engineer. + You have already requested connection via this address! No comment provided by engineer. @@ -6179,11 +8010,6 @@ Repeat join request? Repeat connection request? 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. คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์ @@ -6203,11 +8029,23 @@ Repeat connection request? คุณเข้าร่วมกลุ่มนี้แล้ว กำลังเชื่อมต่อเพื่อเชิญสมาชิกกลุ่ม 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. คุณต้องอนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียงจึงจะสามารถส่งได้ @@ -6223,6 +8061,10 @@ Repeat connection request? คุณส่งคำเชิญเข้าร่วมกลุ่มแล้ว No comment provided by engineer. + + You should receive notifications. + token info + You will be connected to group when the group host's device is online, please wait or check later! คุณจะเชื่อมต่อกับกลุ่มเมื่ออุปกรณ์โฮสต์ของกลุ่มออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง! @@ -6256,6 +8098,10 @@ Repeat connection request? คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน 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. คุณจะหยุดได้รับข้อความจากกลุ่มนี้ ประวัติการแชทจะถูกรักษาไว้ @@ -6276,31 +8122,16 @@ Repeat connection request? คุณกำลังใช้โปรไฟล์ที่ไม่ระบุตัวตนสำหรับกลุ่มนี้ - ไม่อนุญาตให้เชิญผู้ติดต่อเพื่อป้องกันการแชร์โปรไฟล์หลักของคุณ No comment provided by engineer. - - Your %@ servers - เซิร์ฟเวอร์ %@ ของคุณ - No comment provided by engineer. - Your ICE servers เซิร์ฟเวอร์ ICE ของคุณ No comment provided by engineer. - - Your SMP servers - เซิร์ฟเวอร์ SMP ของคุณ - No comment provided by engineer. - Your SimpleX address ที่อยู่ SimpleX ของคุณ No comment provided by engineer. - - Your XFTP servers - เซิร์ฟเวอร์ XFTP ของคุณ - No comment provided by engineer. - Your calls การโทรของคุณ @@ -6316,16 +8147,17 @@ Repeat connection request? ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles โปรไฟล์แชทของคุณ No comment provided by engineer. - - 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). - ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์ -คุณสามารถยกเลิกการเชื่อมต่อนี้และลบผู้ติดต่อออก (และลองใหม่ในภายหลังด้วยลิงก์ใหม่) + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. No comment provided by engineer. @@ -6343,6 +8175,10 @@ You can cancel this connection and remove the contact (and try later with a new ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่ No comment provided by engineer. + + Your credentials may be sent unencrypted. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. ฐานข้อมูลแชทปัจจุบันของคุณจะถูกลบและแทนที่ด้วยฐานข้อมูลที่นำเข้า @@ -6371,33 +8207,34 @@ You can cancel this connection and remove the contact (and try later with a new Your profile **%@** will be shared. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น -เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้ + + Your profile is stored on your device and only shared with your contacts. + โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น No comment provided by engineer. - - Your profile, contacts and delivered messages are stored on your device. - โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ + + 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 was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your random profile โปรไฟล์แบบสุ่มของคุณ No comment provided by engineer. - - Your server - เซิร์ฟเวอร์ของคุณ - No comment provided by engineer. - Your server address ที่อยู่เซิร์ฟเวอร์ของคุณ No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings การตั้งค่าของคุณ @@ -6438,6 +8275,10 @@ SimpleX servers cannot see your profile. รับสายแล้ว call status + + accepted invitation + chat list item title + admin ผู้ดูแลระบบ @@ -6470,6 +8311,14 @@ SimpleX servers cannot see your profile. and %lld other events No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ) @@ -6499,13 +8348,18 @@ SimpleX servers cannot see your profile. blocked by admin - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold ตัวหนา No comment provided by engineer. + + call + No comment provided by engineer. + call error การโทรผิดพลาด @@ -6608,7 +8462,7 @@ SimpleX servers cannot see your profile. connecting… กำลังเชื่อมต่อ… - chat list item title + No comment provided by engineer. connection established @@ -6654,10 +8508,15 @@ SimpleX servers cannot see your profile. วัน time unit + + decryption errors + No comment provided by engineer. + default (%@) ค่าเริ่มต้น (%@) - pref value + delete after time +pref value default (no) @@ -6702,6 +8561,10 @@ SimpleX servers cannot see your profile. ข้อความที่ซ้ำกัน integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted encrypted จากต้นจนจบ @@ -6777,8 +8640,8 @@ SimpleX servers cannot see your profile. ผิดพลาด No comment provided by engineer. - - event happened + + expired No comment provided by engineer. @@ -6810,6 +8673,10 @@ SimpleX servers cannot see your profile. iOS Keychain จะใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัยหลังจากที่คุณรีสตาร์ทแอปหรือเปลี่ยนรหัสผ่าน ซึ่งจะช่วยให้รับการแจ้งเตือนแบบทันทีได้ No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link ไม่ระบุตัวตนผ่านลิงค์ที่อยู่ติดต่อ @@ -6850,6 +8717,10 @@ SimpleX servers cannot see your profile. คำเชิญเข้าร่วมกลุ่ม %@ group name + + invite + No comment provided by engineer. + invited เชิญ @@ -6904,6 +8775,10 @@ SimpleX servers cannot see your profile. เชื่อมต่อสำเร็จ rcv group event chat item + + message + No comment provided by engineer. + message received ข้อความที่ได้รับ @@ -6929,6 +8804,10 @@ SimpleX servers cannot see your profile. กลั่นกรองโดย %@ marked deleted chat item preview text + + moderator + member role + months เดือน @@ -6937,7 +8816,7 @@ SimpleX servers cannot see your profile. never ไม่เคย - No comment provided by engineer. + delete after time new message @@ -6968,8 +8847,8 @@ SimpleX servers cannot see your profile. off ปิด enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -6986,6 +8865,14 @@ SimpleX servers cannot see your profile. เปิด group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner เจ้าของ @@ -7000,6 +8887,14 @@ SimpleX servers cannot see your profile. เพื่อนต่อเพื่อน No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -7014,6 +8909,10 @@ SimpleX servers cannot see your profile. ได้รับการยืนยัน… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call สายถูกปฏิเสธ @@ -7042,6 +8941,10 @@ SimpleX servers cannot see your profile. ลบคุณออกแล้ว rcv group event chat item + + requested to connect + chat list item title + saved No comment provided by engineer. @@ -7050,6 +8953,10 @@ SimpleX servers cannot see your profile. saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec วินาที @@ -7074,6 +8981,12 @@ SimpleX servers cannot see your profile. send direct message No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address profile update event chat item @@ -7110,10 +9023,18 @@ SimpleX servers cannot see your profile. ไม่ทราบ connection info + + unknown servers + No comment provided by engineer. + unknown status No comment provided by engineer. + + unprotected + No comment provided by engineer. + updated group profile อัปเดตโปรไฟล์กลุ่มแล้ว @@ -7152,6 +9073,10 @@ SimpleX servers cannot see your profile. ผ่านรีเลย์ No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) การสนทนาทางวิดีโอ (ไม่ได้ encrypt จากต้นจนจบ) @@ -7177,6 +9102,10 @@ SimpleX servers cannot see your profile. สัปดาห์ time unit + + when IP hidden + No comment provided by engineer. + yes ใช่ @@ -7258,7 +9187,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7294,7 +9223,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7314,4 +9243,205 @@ SimpleX servers cannot see your profile.
+ +
+ +
+ + + %d new events + notification body + + + From %d chat(s) + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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 + 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. + No comment provided by engineer. + + +
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/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index df5076fb07..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,36 +2,9 @@
- +
- - - - - - 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) (kopyalanabilir) @@ -127,9 +100,14 @@ %@ onaylandı No comment provided by engineer. + + %@ server + %@ sunucu + No comment provided by engineer. + %@ servers - %@ sunucuları + %@ sunucular No comment provided by engineer. @@ -142,6 +120,11 @@ %@ bağlanmak istiyor! notification title + + %1$@, %2$@ + %1$@,%2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ ve %lld üyeleri @@ -162,11 +145,36 @@ %d gün time interval + + %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. + forward confirmation reason + %d hours %d saat time interval + + %d messages not forwarded + %d mesajı iletilmeyedi + alert title + %d min %d dakika @@ -182,6 +190,10 @@ %d saniye time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d okunmamış mesaj(lar) @@ -252,11 +264,6 @@ %lld yeni arayüz dilleri No comment provided by engineer. - - %lld second(s) - %lld saniye - No comment provided by engineer. - %lld seconds %lld saniye @@ -307,11 +314,6 @@ %u mesajlar atlandı. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (yeni) @@ -322,33 +324,23 @@ (bu cihaz v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **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. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Yeni kişi ekleyin**: tek seferlik QR Kodunuzu oluşturmak veya kişisel ulaşım bilgileri bağlantısı için. - 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. - - **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. + + **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 notifications server, check messages periodically in the background (depends on how often you use the 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. **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. @@ -362,11 +354,16 @@ **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 notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **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. @@ -392,11 +389,6 @@ \*kalın* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +425,6 @@ - düzenleme geçmişi. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 saniye @@ -445,13 +432,14 @@ 0s - 0 saniye + 0sn No comment provided by engineer. 1 day 1 gün - time interval + delete after time +time interval 1 hour @@ -466,12 +454,28 @@ 1 month 1 ay - time interval + delete after time +time interval 1 week 1 hafta - time interval + delete after time +time interval + + + 1 year + delete after time + + + 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 @@ -488,11 +492,6 @@ 30 saniye No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -542,31 +541,32 @@ Adres değişimi iptal edilsin mi? No comment provided by engineer. - - About SimpleX - SimpleX Hakkında - No comment provided by engineer. - About SimpleX Chat SimpleX Chat hakkında No comment provided by engineer. - - About SimpleX address - SimpleX Chat adresi hakkında + + About operators + Operatörler hakkında No comment provided by engineer. - - Accent color - Vurgu rengi + + Accent + Ana renk No comment provided by engineer. Accept Kabul et accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Koşulları kabul et + No comment provided by engineer. Accept connection request? @@ -581,21 +581,45 @@ Accept incognito Takma adla kabul et - accept contact request via notification + accept contact request via notification +swipe action + + + 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 + token status text + + + 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 contact - Kişi ekle + + Add friends + Arkadaş ekle No comment provided by engineer. - - Add preset servers - Önceden ayarlanmış sunucu ekle + + Add list No comment provided by engineer. @@ -603,14 +627,19 @@ 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 server… - Sunucu ekle… + + Add team members + Takım üyesi ekle No comment provided by engineer. @@ -618,11 +647,45 @@ Başka bir cihaza ekle No comment provided by engineer. + + Add to list + 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 @@ -633,6 +696,16 @@ 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 + No comment provided by engineer. + Admins can block a member for all. Yöneticiler bir üyeyi tamamen engelleyebilirler. @@ -648,6 +721,15 @@ Gelişmiş ağ ayarları No comment provided by engineer. + + Advanced settings + Gelişmiş ayarlar + No comment provided by engineer. + + + All + No comment provided by engineer. + All app data is deleted. Tüm uygulama verileri silinir. @@ -658,16 +740,30 @@ Tüm konuşmalar ve mesajlar silinecektir. Bu, geri alınamaz! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + alert message + 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. 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! @@ -683,6 +779,19 @@ %@ 'den gelen bütün yeni mesajlar saklı olacak! No comment provided by engineer. + + All profiles + Tüm Profiller + profile dropdown + + + 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. Konuştuğun kişilerin tümü bağlı kalacaktır. @@ -695,7 +804,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Tüm kişileriniz, sohbetleriniz ve dosyalarınız güvenli bir şekilde şifrelenecek ve parçalar halinde yapılandırılmış XFTP rölelerine yüklenecektir. + 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. @@ -708,11 +817,21 @@ 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) @@ -730,7 +849,7 @@ Allow sending direct messages to members. - Üyelere direkt mesaj göndermeye izin ver. + Üyelere doğrudan mesaj göndermeye izin ver. No comment provided by engineer. @@ -738,11 +857,20 @@ 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 report messsages to moderators. + No comment provided by engineer. + Allow to send SimpleX links. SimpleX bağlantıları göndilmesine izin ver. @@ -808,6 +936,11 @@ Zaten gruba bağlanılıyor! 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 @@ -818,11 +951,20 @@ Verilen adla boş bir sohbet profili oluşturulur ve uygulama her zamanki gibi açılır. No comment provided by engineer. + + Another reason + report reason + 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ü: %@ @@ -838,6 +980,10 @@ Uygulama yerel dosyaları şifreler (videolar dışında). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon Uygulama simgesi @@ -853,6 +999,11 @@ 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ü @@ -873,11 +1024,50 @@ Uygula No comment provided by engineer. + + Apply to + Şuna uygula + No comment provided by engineer. + + + Archive + No comment provided by engineer. + + + Archive %lld reports? + No comment provided by engineer. + + + Archive all reports? + 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. + + + Archive report + No comment provided by engineer. + + + Archive report? + No comment provided by engineer. + + + Archive reports + swipe action + + + Archived contacts + Arşivli kişiler + No comment provided by engineer. + Archiving database Veritabanı arşivleniyor @@ -943,11 +1133,21 @@ Fotoğrafları otomatik kabul et No comment provided by engineer. + + Auto-accept settings + Ayarları otomatik olarak kabul et + alert title + Back Geri No comment provided by engineer. + + Background + Arka plan + No comment provided by engineer. + Bad desktop address Kötü bilgisayar adresi @@ -963,16 +1163,59 @@ Kötü mesaj karması 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 groups performance + 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 privacy and security + 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 @@ -1008,6 +1251,16 @@ 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. + Both you and your contact can add message reactions. Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir. @@ -1038,11 +1291,31 @@ 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. + + + 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). Sohbet profiline göre (varsayılan) veya [bağlantıya göre](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. + + By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! Arama çoktan bitti! @@ -1053,11 +1326,26 @@ 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! @@ -1068,10 +1356,16 @@ Kişiler davet edilemiyor! No comment provided by engineer. + + Can't message member + Üyeye mesaj gönderilemiyor + No comment provided by engineer. + Cancel İptal et - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1083,10 +1377,20 @@ 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. + Cannot receive file Dosya alınamıyor - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Kapasite aşıldı - alıcı önceden gönderilen mesajları almadı. + snd error text Cellular @@ -1098,6 +1402,15 @@ Değiştir No comment provided by engineer. + + Change automatic message deletion? + alert title + + + Change chat profiles + Sohbet profillerini değiştir + authentication reason + Change database passphrase? Veritabanı parolasını değiştir? @@ -1142,11 +1455,26 @@ Change self-destruct passcode Kendini yok eden parolayı değiştir authentication reason - set passcode view +set passcode view - - Chat archive - Sohbet arşivi + + 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. @@ -1164,6 +1492,11 @@ 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ı @@ -1184,6 +1517,11 @@ 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ı! @@ -1194,15 +1532,50 @@ Sohbet tercihleri No comment provided by engineer. + + Chat preferences were changed. + Sohbet tercihleri değiştirildi. + alert message + + + 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. + No comment provided by engineer. + Check server address and try again. Sunucu adresini kontrol edip tekrar deneyin. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1224,10 +1597,25 @@ 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 + No comment provided by engineer. + Clear Temizle - No comment provided by engineer. + swipe action Clear conversation @@ -1239,6 +1627,14 @@ Sohbet temizlensin mi? No comment provided by engineer. + + Clear group? + No comment provided by engineer. + + + Clear or delete group? + No comment provided by engineer. + Clear private notes? Gizli notlar temizlensin mi? @@ -1249,11 +1645,20 @@ Doğrulamayı temizle No comment provided by engineer. - - Colors - Renkler + + Color chats with the new themes. + Yeni temalarla renkli sohbetler. No comment provided by engineer. + + Color mode + Renk modu + No comment provided by engineer. + + + Community guidelines violation + report reason + Compare file Dosya karşılaştır @@ -1264,11 +1669,55 @@ Güvenlik kodlarını kişilerinle karşılaştır. 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. + + Configure server operators + No comment provided by engineer. + Confirm Onayla @@ -1279,11 +1728,21 @@ Parolayı 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 @@ -1309,6 +1768,10 @@ Yüklemeyi onayla No comment provided by engineer. + + Confirmed + token status text + Connect Bağlan @@ -1329,6 +1792,11 @@ Bilgisayara 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? @@ -1368,14 +1836,29 @@ Bu senin kendi tek kullanımlık bağlantın! %@ ile bağlan No comment provided by engineer. + + Connected + Bağlandı + No comment provided by engineer. + Connected desktop Bilgisayara bağlandı No comment provided by engineer. + + Connected servers + Bağlı sunucular + No comment provided by engineer. + Connected to desktop - Bilgisayara bağlanıldı + Masaüstüne bağlandı + No comment provided by engineer. + + + Connecting + Bağlanıyor No comment provided by engineer. @@ -1388,6 +1871,11 @@ Bu senin kendi tek kullanımlık bağlantın! 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 @@ -1398,6 +1886,15 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı No comment provided by engineer. + + Connection and servers status. + Bağlantı ve sunucuların durumu. + No comment provided by engineer. + + + Connection blocked + No comment provided by engineer. + Connection error Bağlantı hatası @@ -1408,11 +1905,34 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı hatası (DOĞRULAMA) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + No comment provided by engineer. + + + Connection not ready. + 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 requires encryption renegotiation. + No comment provided by engineer. + + + Connection security + Bağlantı güvenliği + No comment provided by engineer. + Connection terminated Bağlantı sonlandırılmış @@ -1423,6 +1943,16 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı süresi geçmiş No comment provided by engineer. + + Connection with desktop stopped + Masaüstü ile bağlantı durduruldu + No comment provided by engineer. + + + Connections + Bağlantılar + No comment provided by engineer. + Contact allows Kişi izin veriyor @@ -1433,6 +1963,11 @@ Bu senin kendi tek kullanımlık bağlantın! Kişi zaten mevcut No comment provided by engineer. + + Contact deleted! + Kişiler silindi! + No comment provided by engineer. + Contact hidden: Kişi gizli: @@ -1443,9 +1978,9 @@ Bu senin kendi tek kullanımlık bağlantın! Kişi bağlandı notification - - Contact is not connected yet! - Kişi şuan bağlanmadı! + + Contact is deleted. + Kişi silindi. No comment provided by engineer. @@ -1458,6 +1993,11 @@ Bu senin kendi tek kullanımlık bağlantın! 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 @@ -1468,21 +2008,40 @@ Bu senin kendi tek kullanımlık bağlantın! Kişiler silinmesi için mesajları işaretleyebilir; onları görüntüleyebilirsin. No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue Devam et No comment provided by engineer. + + Conversation deleted! + Sohbet silindi! + No comment provided by engineer. + Copy Kopyala - chat item action + 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? @@ -1493,6 +2052,11 @@ Bu senin kendi tek kullanımlık bağlantın! 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 SimpleX address SimpleX adresi oluştur @@ -1503,11 +2067,6 @@ Bu senin kendi tek kullanımlık bağlantın! Rasgele profil kullanarak grup oluştur. No comment provided by engineer. - - Create an address to let people connect with you. - İnsanların seninle bağlanması için bir adres oluştur. - No comment provided by engineer. - Create file Dosya oluştur @@ -1528,6 +2087,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı oluştur No comment provided by engineer. + + Create list + 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/). 💻 @@ -1553,6 +2116,11 @@ Bu senin kendi tek kullanımlık bağlantın! Profilini oluştur No comment provided by engineer. + + Created + Yaratıldı + No comment provided by engineer. + Created at Şurada oluşturuldu @@ -1563,11 +2131,6 @@ Bu senin kendi tek kullanımlık bağlantın! Şurada oluşturuldu: %@ copied message info - - Created on %@ - %@ de oluşturuldu - No comment provided by engineer. - Creating archive link Arşiv bağlantısı oluşturuluyor @@ -1583,11 +2146,21 @@ Bu senin kendi tek kullanımlık bağlantın! Şu anki şifre 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 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. @@ -1598,11 +2171,26 @@ Bu senin kendi tek kullanımlık bağlantın! Ö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 ID Veritabanı kimliği @@ -1701,6 +2289,11 @@ Bu senin kendi tek kullanımlık bağlantın! Uygulama yeniden başlatıldığında veritabanı taşınacaktır No comment provided by engineer. + + Debug delivery + Hata ayıklama teslimatı + No comment provided by engineer. + Decentralized Merkezi Olmayan @@ -1714,18 +2307,19 @@ Bu senin kendi tek kullanımlık bağlantın! Delete Sil - chat item action + alert action +swipe action + + + 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 Contact - Kişiyi sil - No comment provided by engineer. - Delete address Adresi sil @@ -1751,14 +2345,13 @@ Bu senin kendi tek kullanımlık bağlantın! Sil ve kişiye bildir No comment provided by engineer. - - Delete archive - Arşivi sil + + Delete chat + Sohbeti sil No comment provided by engineer. - - Delete chat archive? - Sohbet arşivi silinsin mi? + + Delete chat messages from your device. No comment provided by engineer. @@ -1771,6 +2364,11 @@ Bu senin kendi tek kullanımlık bağlantın! 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 @@ -1781,11 +2379,9 @@ Bu senin kendi tek kullanımlık bağlantın! Kişiyi sil No comment provided by engineer. - - Delete contact? -This cannot be undone! - Kişi silinsin mi? -Bu geri alınamaz! + + Delete contact? + Kişiyi sil? No comment provided by engineer. @@ -1848,6 +2444,10 @@ Bu geri alınamaz! Bağlantı silinsin mi? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Kişinin mesajı silinsin mi? @@ -1861,7 +2461,7 @@ Bu geri alınamaz! Delete messages Mesajları sil - No comment provided by engineer. + alert button Delete messages after @@ -1878,9 +2478,9 @@ Bu geri alınamaz! Eski veritabanı silinsin mi? No comment provided by engineer. - - Delete pending connection - Bekleyen bağlantıyı sil + + Delete or moderate up to 200 messages. + 200'e kadar mesajı silin veya düzenleyin. No comment provided by engineer. @@ -1898,11 +2498,30 @@ Bu geri alınamaz! Sırayı sil server test step + + Delete report + 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 + No comment provided by engineer. + + + Deleted + Silindi + No comment provided by engineer. + Deleted at de silindi @@ -1913,6 +2532,15 @@ Bu geri alınamaz! %@ de silindi copied message info + + Deletion errors + Silme hatası + No comment provided by engineer. + + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Teslimat @@ -1948,11 +2576,41 @@ Bu geri alınamaz! 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. + No comment provided by engineer. + + + Destination server error: %@ + Hedef sunucu hatası: %@ + snd error text + + + 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ı @@ -1983,8 +2641,13 @@ Bu geri alınamaz! Doğrudan mesajlar chat feature - - Direct messages between members are prohibited in this group. + + 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. @@ -1998,11 +2661,24 @@ Bu geri alınamaz! SimpleX Kilidini devre dışı bırak authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Herkes için devre dışı bırak No comment provided by engineer. + + Disabled + Devre dışı + No comment provided by engineer. + Disappearing message Kaybolan mesaj @@ -2018,8 +2694,8 @@ Bu geri alınamaz! Kaybolan mesajlar bu sohbette yasaklanmış. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Kaybolan mesajlar bu grupta yasaklanmış. No comment provided by engineer. @@ -2053,11 +2729,21 @@ Bu geri alınamaz! Yerel ağ aracılığıyla keşfet 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 SimpleX for emergency calls. Acil aramalar için SimpleX'i KULLANMAYIN. No comment provided by engineer. + + Do NOT use private routing. + Gizli yönlendirmeyi KULLANMA. + No comment provided by engineer. + Do it later Sonra yap @@ -2068,6 +2754,15 @@ Bu geri alınamaz! Yeni üyelere geçmişi gönderme. No comment provided by engineer. + + Do not use credentials with proxy. + Kimlik bilgilerini proxy ile kullanmayın. + No comment provided by engineer. + + + Documents: + No comment provided by engineer. + Don't create address Adres oluşturma @@ -2078,11 +2773,19 @@ Bu geri alınamaz! Etkinleştirme No comment provided by engineer. + + Don't miss important messages. + No comment provided by engineer. + Don't show again Yeniden gösterme No comment provided by engineer. + + Done + No comment provided by engineer. + Downgrade and open chat Sürüm düşür ve sohbeti aç @@ -2091,7 +2794,13 @@ Bu geri alınamaz! Download İndir - chat item action + alert button +chat item action + + + Download errors + İndirme hataları + No comment provided by engineer. Download failed @@ -2103,6 +2812,21 @@ Bu geri alınamaz! Dosya indir server test step + + Download files + Dosyaları indirin + alert action + + + Downloaded + İndirildi + No comment provided by engineer. + + + Downloaded files + Dosyalar İndirildi + No comment provided by engineer. + Downloading archive Arşiv indiriliyor @@ -2123,6 +2847,11 @@ Bu geri alınamaz! Süre No comment provided by engineer. + + E2E encrypted notifications. + Uçtan uca şifrelenmiş bildirimler. + No comment provided by engineer. + Edit Düzenle @@ -2143,6 +2872,10 @@ Bu geri alınamaz! Etkinleştir (geçersiz kılmaları koru) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock SimpleX Kilidini etkinleştir @@ -2156,7 +2889,7 @@ Bu geri alınamaz! Enable automatic message deletion? Otomatik mesaj silme etkinleştirilsin mi? - No comment provided by engineer. + alert title Enable camera access @@ -2203,6 +2936,11 @@ Bu geri alınamaz! Kendini imha şifresini etkinleştir set passcode view + + Enabled + Etkin + No comment provided by engineer. + Enabled for Şunlar için etkinleştirildi @@ -2278,6 +3016,10 @@ Bu geri alınamaz! Şifreleme yeniden anlaşma başarısız oldu. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Şifre gir @@ -2343,26 +3085,36 @@ Bu geri alınamaz! Adres değişikliği iptal edilirken hata oluştu No comment provided by engineer. + + Error accepting conditions + Koşulları kabul ederken hata oluştu + alert title + Error accepting contact request Bağlantı isteği kabul edilirken hata oluştu No comment provided by engineer. - - Error accessing database file - Veritabanı dosyasına erişilirken hata oluştu - No comment provided by engineer. - Error adding member(s) Üye(ler) eklenirken hata oluştu No comment provided by engineer. + + Error adding server + Sunucu eklenirken hata oluştu + alert title + 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 @@ -2373,6 +3125,20 @@ Bu geri alınamaz! 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 checking token status + 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 @@ -2388,6 +3154,10 @@ Bu geri alınamaz! Grup bağlantısı oluşturulurken hata oluştu No comment provided by engineer. + + Error creating list + alert title + Error creating member contact Kişi iletişimi oluşturulurken hata oluştu @@ -2403,6 +3173,10 @@ Bu geri alınamaz! Profil oluşturulurken hata oluştu! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Dosya şifresi çözülürken hata oluştu @@ -2423,11 +3197,6 @@ Bu geri alınamaz! Bağlantı silinirken hata oluştu No comment provided by engineer. - - Error deleting contact - Kişi silinirken hata oluştu - No comment provided by engineer. - Error deleting database Veritabanı silinirken hata oluştu @@ -2473,6 +3242,11 @@ Bu geri alınamaz! 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 @@ -2483,9 +3257,14 @@ Bu geri alınamaz! Gruba katılırken hata oluştu No comment provided by engineer. - - Error loading %@ servers - %@ sunucuları yüklenirken hata oluştu + + Error loading servers + Sunucular yüklenirken hata oluştu + alert title + + + Error migrating settings + Ayarlar taşınırken hata oluştu No comment provided by engineer. @@ -2496,16 +3275,34 @@ Bu geri alınamaz! Error receiving file Dosya alınırken sorun oluştu + alert title + + + 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 registering for notifications + alert title + Error removing member Kişiyi silerken sorun oluştu No comment provided by engineer. - - Error saving %@ servers - %@ sunucuları kaydedilirken sorun oluştu + + Error reordering lists + alert title + + + Error resetting statistics + Hata istatistikler sıfırlanıyor No comment provided by engineer. @@ -2513,6 +3310,10 @@ Bu geri alınamaz! ICE sunucularını kaydedirken sorun oluştu No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile Grup profili kaydedilirken sorun oluştu @@ -2528,6 +3329,11 @@ Bu geri alınamaz! Parolayı Anahtar Zincirine kaydederken hata oluştu No comment provided by engineer. + + Error saving servers + Sunucular kaydedilirken hata oluştu + alert title + Error saving settings Ayarlar kaydedilirken hata oluştu @@ -2573,16 +3379,25 @@ Bu geri alınamaz! Sohbet durdurulurken hata oluştu No comment provided by engineer. + + Error switching profile + Profil değiştirme sırasında hata oluştu + No comment provided by engineer. + Error switching profile! Profil değiştirilirken hata oluştu! - No comment provided by engineer. + alertTitle Error synchronizing connection Bağlantı senkronizasyonunda hata oluştu No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link Grup bağlantısı güncellenirken hata oluştu @@ -2593,6 +3408,11 @@ Bu geri alınamaz! Mesaj güncellenirken hata oluştu No comment provided by engineer. + + Error updating server + Sunucu güncellenirken hata oluştu + alert title + Error updating settings Ayarları güncellerken hata oluştu @@ -2621,7 +3441,9 @@ Bu geri alınamaz! Error: %@ Hata: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2633,6 +3455,16 @@ Bu geri alınamaz! Hata: veritabanı dosyası yok No comment provided by engineer. + + Errors + Hatalar + No comment provided by engineer. + + + Errors in servers configuration. + Sunucular yapılandırılırken hatalar oluştu. + servers error + Even when disabled in the conversation. Konuşma sırasında devre dışı bırakılsa bile. @@ -2648,6 +3480,10 @@ Bu geri alınamaz! Genişlet chat item action + + Expired + token status text + Export database Veritabanını dışarı aktar @@ -2658,6 +3494,11 @@ Bu geri alınamaz! 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. @@ -2683,16 +3524,65 @@ Bu geri alınamaz! Hızlı ve gönderici çevrimiçi olana kadar beklemek yok! No comment provided by engineer. + + Faster deletion of groups. + No comment provided by engineer. + Faster joining and more reliable messages. Daha hızlı katılma ve daha güvenilir mesajlar. No comment provided by engineer. + + Faster sending messages. + No comment provided by engineer. + Favorite Favori + swipe action + + + Favorites No comment provided by engineer. + + File error + Dosya hatası + file error alert title + + + File errors: +%@ + Dosya hataları: +%@ + alert message + + + File is blocked by server operator: +%@. + 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ı: %@ + file error text + + + File status + Dosya durumu + No comment provided by engineer. + + + File status: %@ + Dosya durumu: %@ + copied message info + File will be deleted from servers. Dosya sunuculardan silinecek. @@ -2713,6 +3603,11 @@ Bu geri alınamaz! Dosya: %@ No comment provided by engineer. + + Files + Dosyalar + No comment provided by engineer. + Files & media Dosyalar & medya @@ -2723,8 +3618,8 @@ Bu geri alınamaz! Dosyalar ve medya chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Dosyalar ve medya bu grupta yasaklandı. No comment provided by engineer. @@ -2793,21 +3688,69 @@ Bu geri alınamaz! Düzeltme grup üyesi tarafından desteklenmiyor No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + Sohbet profili için %@: + servers error + 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 me + 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 + No comment provided by engineer. + Forward İlet chat item action + + Forward %d message(s)? + %d mesaj(lar)ı iletilsin mi? + alert title + Forward and save messages Mesajları ilet ve kaydet No comment provided by engineer. + + Forward messages + İletileri ilet + alert action + + + Forward messages without files? + Mesajlar dosyalar olmadan iletilsin mi ? + alert message + + + Forward up to 20 messages at once. + Aynı anda en fazla 20 mesaj iletin. + No comment provided by engineer. + Forwarded İletildi @@ -2818,6 +3761,40 @@ Bu geri alınamaz! Ş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: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Yönlendirme sunucusu: %1$@ +Hedef sunucu hatası: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Yönlendirme sunucusu: %1$@ +Hata: %2$@ + snd error text + Found desktop Bilgisayar bulundu @@ -2838,11 +3815,6 @@ Bu geri alınamaz! Bütün isim (opsiyonel) No comment provided by engineer. - - Full name: - Bütün isim: - No comment provided by engineer. - Fully decentralized – visible only to members. Tamamiyle merkezi olmayan - sadece kişilere görünür. @@ -2863,6 +3835,20 @@ Bu geri alınamaz! GİFler ve çıkartmalar No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + İyi öğlenler! + message preview + + + Good morning! + Günaydın! + message preview + Group Grup @@ -2918,41 +3904,6 @@ Bu geri alınamaz! Grup bağlantıları No comment provided by engineer. - - Group members can add message reactions. - Grup üyeleri mesaj tepkileri ekleyebilir. - No comment provided by engineer. - - - Group 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. - - - Group members can send SimpleX links. - Grup üyeleri SimpleX bağlantıları gönderebilir. - No comment provided by engineer. - - - Group members can send direct messages. - Grup üyeleri doğrudan mesajlar gönderebilir. - No comment provided by engineer. - - - Group members can send disappearing messages. - Grup üyeleri kaybolan mesajlar gönderebilir. - No comment provided by engineer. - - - Group members can send files and media. - Grup üyeleri dosyalar ve medya gönderebilir. - No comment provided by engineer. - - - Group members can send voice messages. - Grup üyeleri sesli mesajlar gönderebilir. - No comment provided by engineer. - Group message: Grup mesajı: @@ -2993,11 +3944,19 @@ Bu geri alınamaz! Grup senden silinecektir - bu geri alınamaz! No comment provided by engineer. + + Groups + No comment provided by engineer. + Help Yardım No comment provided by engineer. + + Help admins moderating their groups. + No comment provided by engineer. + Hidden Gizlenmiş @@ -3048,10 +4007,19 @@ Bu geri alınamaz! SimpleX nasıl çalışır 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 it works - Nasıl çalışıyor - No comment provided by engineer. + alert button How to @@ -3078,6 +4046,11 @@ Bu geri alınamaz! ICE sunucuları (her satıra bir tane) No comment provided by engineer. + + IP address + IP adresi + 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ş. @@ -3118,8 +4091,8 @@ Bu geri alınamaz! Hemen No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Spam ve kötüye kullanıma karşı bağışıklı No comment provided by engineer. @@ -3143,11 +4116,23 @@ Bu geri alınamaz! İç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. +More improvements are coming soon! + İyileştirilmiş teslimat, azaltılmış trafik kullanımı. +Daha fazla iyileştirme yakında geliyor! + No comment provided by engineer. + Improved message delivery İyileştirilmiş mesaj iletimi @@ -3178,6 +4163,14 @@ Bu geri alınamaz! Arama içi sesler No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Gizli @@ -3248,6 +4241,11 @@ Bu geri alınamaz! [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! @@ -3255,16 +4253,36 @@ Bu geri alınamaz! No comment provided by engineer. - - Instantly - Anında - No comment provided by engineer. - Interface Arayüz No comment provided by engineer. + + Interface colors + Arayüz renkleri + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code Geçersiz QR kodu @@ -3303,7 +4321,7 @@ Bu geri alınamaz! Invalid server address! Geçersiz sunucu adresi! - No comment provided by engineer. + alert title Invalid status @@ -3325,6 +4343,11 @@ Bu geri alınamaz! Ü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 @@ -3340,8 +4363,8 @@ Bu geri alınamaz! Bu sohbette geri döndürülemez mesaj silme yasaktır. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Bu grupta geri döndürülemez mesaj silme yasaktır. No comment provided by engineer. @@ -3366,6 +4389,11 @@ Bu geri alınamaz! 3. 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 (%@). @@ -3384,7 +4412,7 @@ Bu geri alınamaz! Join Katıl - No comment provided by engineer. + swipe action Join group @@ -3426,6 +4454,11 @@ Bu senin grup için bağlantın %@! Keep Tut + alert action + + + Keep conversation + Sohbeti sakla No comment provided by engineer. @@ -3436,7 +4469,7 @@ Bu senin grup için bağlantın %@! Keep unused invitation? Kullanılmamış davet tutulsun mu? - No comment provided by engineer. + alert title Keep your connections @@ -3471,6 +4504,16 @@ Bu senin grup için bağlantın %@! Leave Ayrıl + swipe action + + + Leave chat + Sohbetten ayrıl + No comment provided by engineer. + + + Leave chat? + Sohbetten ayrılsın mı? No comment provided by engineer. @@ -3513,6 +4556,18 @@ Bu senin grup için bağlantın %@! Bağlanmış bilgisayarlar No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! Canlı mesaj! @@ -3523,11 +4578,6 @@ Bu senin grup için bağlantın %@! Canlı mesajlar No comment provided by engineer. - - Local - Yerel - No comment provided by engineer. - Local name Yerel isim @@ -3548,11 +4598,6 @@ Bu senin grup için bağlantın %@! Kilit modu No comment provided by engineer. - - Make a private connection - Gizli bir bağlantı oluştur - No comment provided by engineer. - Make one message disappear Bir mesajın kaybolmasını sağlayın @@ -3563,21 +4608,11 @@ Bu senin grup için bağlantın %@! Profili gizli yap! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - %@ sunucu adreslerinin doğru formatta olduğundan, satır ayrımı yapıldığından ve yinelenmediğinden (%@) emin olun. - 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. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Çoğu kişi sordu: *eğer SimpleX'in hiç kullanıcı tanımlayıcıları yok, o zaman mesajları nasıl gönderebiliyor?* - No comment provided by engineer. - Mark deleted for everyone Herkes için silinmiş olarak işaretle @@ -3603,11 +4638,35 @@ Bu senin grup için bağlantın %@! Maksimum 30 saniye, anında alındı. No comment provided by engineer. + + Media & file servers + Medya ve dosya sunucuları + No comment provided by engineer. + + + Medium + Orta + blur media + Member Kişi No comment provided by engineer. + + Member inactive + Üye inaktif + item status text + + + Member reports + chat feature + + + 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. @@ -3618,11 +4677,63 @@ Bu senin grup için bağlantın %@! Üye rolü "%@" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır. 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! Ü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 report messsages to moderators. + 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 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 voice messages. + Grup üyeleri sesli mesajlar gönderebilir. + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + Menüler + No comment provided by engineer. + Message delivery error Mesaj gönderim hatası @@ -3633,11 +4744,31 @@ Bu senin grup için bağlantın %@! Mesaj alındı bilgisi! No comment provided by engineer. + + Message delivery warning + Mesaj iletimi uyarısı + item status text + Message draft Mesaj taslağı No comment provided by engineer. + + Message forwarded + Mesaj iletildi + item status text + + + Message may be delivered later if member becomes active. + Kullanıcı aktif olursa mesaj iletilebilir. + item status description + + + Message queue info + Mesaj kuyruğu bilgisi + No comment provided by engineer. + Message reactions Mesaj tepkileri @@ -3648,16 +4779,41 @@ Bu senin grup için bağlantın %@! Mesaj tepkileri bu sohbette yasaklandı. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Mesaj tepkileri bu grupta yasaklandı. 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 + No comment provided by engineer. + + + Message status: %@ + Mesaj durumu: %@ + copied message info + Message text Mesaj yazısı @@ -3683,6 +4839,25 @@ Bu senin grup için bağlantın %@! %@ den gelen mesajlar gösterilecektir! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + Mesajlar alındı + No comment provided by engineer. + + + Messages sent + Mesajlar gönderildi + No comment provided by engineer. + + + Messages were deleted after you selected them. + Mesajlar siz seçtikten sonra silindi. + alert message + 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. @@ -3748,9 +4923,9 @@ Bu senin grup için bağlantın %@! Geçiş tamamlandı No comment provided by engineer. - - Migrations: %@ - Geçişler: %@ + + Migrations: + Geçişler: No comment provided by engineer. @@ -3768,6 +4943,10 @@ Bu senin grup için bağlantın %@! %@ de yönetildi copied message info + + More + swipe action + More improvements are coming soon! Daha fazla geliştirmeler yakında geliyor! @@ -3778,16 +4957,15 @@ Bu senin grup için bağlantın %@! Daha güvenilir ağ bağlantısı. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Büyük ihtimalle bu bağlantı silinmiş. item status description - - Most likely this contact has deleted the connection with you. - Büyük ihtimalle bu kişi seninle bağlantını sildi. - No comment provided by engineer. - Multiple chat profiles Çoklu sohbet profili @@ -3796,7 +4974,11 @@ Bu senin grup için bağlantın %@! Mute Sustur - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3806,7 +4988,7 @@ Bu senin grup için bağlantın %@! Name İsim - No comment provided by engineer. + swipe action Network & servers @@ -3818,11 +5000,24 @@ Bu senin grup için bağlantın %@! Ağ bağlantısı No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu. + snd error text + Network management Ağ yönetimi No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Ağ ayarları @@ -3833,16 +5028,35 @@ Bu senin grup için bağlantın %@! Ağ durumu No comment provided by engineer. + + New + token status text + New Passcode Yeni şifre 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. + No comment provided by engineer. + New chat Yeni sohbet No comment provided by engineer. + + New chat experience 🎉 + Yeni bir sohbet deneyimi 🎉 + No comment provided by engineer. + New contact request Yeni bağlantı isteği @@ -3853,11 +5067,6 @@ Bu senin grup için bağlantın %@! Yeni kişi: notification - - New database archive - Yeni veritabanı arşivi - No comment provided by engineer. - New desktop app! Yeni bilgisayar uygulaması! @@ -3868,11 +5077,20 @@ Bu senin grup için bağlantın %@! Yeni görünen ad No comment provided by engineer. + + New events + notification + 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ü @@ -3888,6 +5106,10 @@ Bu senin grup için bağlantın %@! Yeni parola… No comment provided by engineer. + + New server + No comment provided by engineer. + No Hayır @@ -3898,6 +5120,18 @@ Bu senin grup için bağlantın %@! Uygulama şifresi yok Authentication unavailable + + 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 Hiçbir kişi seçilmedi @@ -3918,6 +5152,11 @@ Bu senin grup için bağlantın %@! Cihaz tokeni yok! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Henüz direkt bağlantı yok mesaj admin tarafından yönlendirildi. + item status description + No filtered chats Filtrelenmiş sohbetler yok @@ -3933,26 +5172,101 @@ Bu senin grup için bağlantın %@! Geçmiş yok No comment provided by engineer. + + No info, try to reload + Bilgi yok, yenilemeyi deneyin + No comment provided by engineer. + + + No media & file servers. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + servers error + 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 No comment provided by engineer. + + 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. + servers error + + + No token! + alert title + + + No unread chats + 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. + + Notes + No comment provided by engineer. + + + Nothing selected + Hiçbir şey seçilmedi + No comment provided by engineer. + + + Nothing to forward! + Yönlendirilecek bir şey yok! + alert title + Notifications Bildirimler @@ -3963,6 +5277,18 @@ Bu senin grup için bağlantın %@! Bildirimler devre dışı! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3980,36 +5306,35 @@ Bu senin grup için bağlantın %@! Off Kapalı - No comment provided by engineer. + blur media Ok Tamam - No comment provided by engineer. + alert button Old database Eski veritabanı No comment provided by engineer. - - Old database archive - Eski veritabanı arşivi - 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. Requires enabling VPN. - Bağlantı için Onion ana bilgisayarları gerekecektir. VPN'nin etkinleştirilmesi gerekir. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Bağlantı için Onion ana bilgisayarları gerekecektir. +VPN'nin etkinleştirilmesi gerekir. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion ana bilgisayarları mevcutsa kullanılacaktır. VPN'nin etkinleştirilmesi gerekir. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion ana bilgisayarları mevcutsa kullanılacaktır. +VPN'nin etkinleştirilmesi gerekir. No comment provided by engineer. @@ -4017,11 +5342,20 @@ Bu senin grup için bağlantın %@! Onion ana bilgisayarları kullanılmayacaktır. 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. 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. @@ -4037,6 +5371,14 @@ Bu senin grup için bağlantın %@! Yalnızca grup sahipleri sesli mesajları etkinleştirebilir. 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. Sadece siz mesaj tepkileri ekleyebilirsiniz. @@ -4090,13 +5432,17 @@ Bu senin grup için bağlantın %@! Open - No comment provided by engineer. + alert action Open Settings Ayarları aç No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Sohbeti aç @@ -4107,31 +5453,41 @@ Bu senin grup için bağlantın %@! Sohbet konsolunu aç authentication reason + + Open conditions + No comment provided by engineer. + Open group Grubu aç No comment provided by engineer. + + Open link? + alert title + Open migration to another device Başka bir cihaza açık geçiş authentication reason - - Open user profiles - Kullanıcı profillerini aç - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir. - No comment provided by engineer. - Opening app… Uygulama açılıyor… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + + + Or import archive file + No comment provided by engineer. + Or paste archive link Veya arşiv bağlantısını yapıştırın @@ -4152,11 +5508,26 @@ Bu senin grup için bağlantın %@! Veya bu kodu göster No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + + + Organize chats into lists + No comment provided by engineer. + Other Diğer No comment provided by engineer. + + Other file errors: +%@ + Diğer dosya hataları: +%@ + alert message + PING count PING sayısı @@ -4192,6 +5563,11 @@ Bu senin grup için bağlantın %@! Şifre ayarlandı! No comment provided by engineer. + + Password + Şifre + No comment provided by engineer. + Password to show Gösterilecek şifre @@ -4222,13 +5598,13 @@ Bu senin grup için bağlantın %@! Aldığın bağlantıyı yapıştır No comment provided by engineer. - - People can connect to you only via the links you share. - İnsanlar size yalnızca paylaştığınız bağlantılar üzerinden ulaşabilir. + + Pending + Bekleniyor No comment provided by engineer. - - Periodically + + Periodic Periyodik olarak No comment provided by engineer. @@ -4242,11 +5618,28 @@ Bu senin grup için bağlantın %@! Resim içinde resim aramaları 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. +Please 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. +Lü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. @@ -4314,11 +5707,28 @@ Hata: %@ Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz parolayı DEĞİŞTİREMEZSİNİZ. No comment provided by engineer. + + 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. + token info + Polish interface Lehçe arayüz No comment provided by engineer. + + Port + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil @@ -4329,41 +5739,81 @@ Hata: %@ Son mesaj taslağını ekleriyle birlikte koru. No comment provided by engineer. - - Preset server - Ön ayarlı sunucu - No comment provided by engineer. - Preset server address Ön ayarlı sunucu adresi No comment provided by engineer. + + Preset servers + 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 for your customers. + No comment provided by engineer. + + + Privacy policy and conditions of use. + No comment provided by engineer. + Privacy redefined Gizlilik yeniden tanımlandı No comment provided by engineer. + + Private chats, groups and your contacts are not accessible to server operators. + No comment provided by engineer. + Private filenames Gizli dosya adları No comment provided by engineer. + + Private media file names. + No comment provided by engineer. + + + Private message routing + Gizli mesaj yönlendirme + No comment provided by engineer. + + + Private message routing 🚀 + Gizli mesaj yönlendirme 🚀 + No comment provided by engineer. + Private notes Gizli notlar name of notes to self + + 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ı @@ -4379,25 +5829,20 @@ Hata: %@ Profil resimleri No comment provided by engineer. - - Profile name - Profil ismi - No comment provided by engineer. - - - Profile name: - Profil ismi: - No comment provided by engineer. - Profile password Profil parolası No comment provided by engineer. + + Profile theme + Profil teması + No comment provided by engineer. + Profile update will be sent to your contacts. Profil güncellemesi kişilerinize gönderilecektir. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4419,6 +5864,10 @@ Hata: %@ Mesajlarda tepkileri yasakla. No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. SimpleX bağlantısı gönderimini yasakla. @@ -4426,7 +5875,7 @@ Hata: %@ Prohibit sending direct messages to members. - Geri dönülmez mesaj silme işlemini yasakla. + Üyelere doğrudan mesaj göndermeyi yasakla. No comment provided by engineer. @@ -4444,11 +5893,23 @@ Hata: %@ Sesli mesajların gönderimini yasakla. No comment provided by engineer. + + Protect IP address + IP adresini koru + No comment provided by engineer. + Protect app screen Uygulama ekranını koru No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + IP adresinizi kişileriniz tarafından seçilen mesajlaşma yönlendiricilerinden koruyun. +*Ağ ve sunucular* ayarlarında etkinleştirin. + No comment provided by engineer. + Protect your chat profiles with a password! Bir parolayla birlikte sohbet profillerini koru! @@ -4464,6 +5925,21 @@ Hata: %@ 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 @@ -4484,6 +5960,11 @@ Hata: %@ Uygulamayı değerlendir No comment provided by engineer. + + Reachable chat toolbar + Erişilebilir sohbet araç çubuğu + No comment provided by engineer. + React… Tepki ver… @@ -4492,33 +5973,28 @@ Hata: %@ Read Oku - No comment provided by engineer. + swipe action Read more Dahasını oku No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - [Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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. - Daha fazlasını GitHub depomuzdan oku. - 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). @@ -4526,7 +6002,12 @@ Hata: %@ Receipts are disabled - Gönderildi bilgisi devre dışı bırakıldı + 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. @@ -4549,16 +6030,26 @@ Hata: %@ Mesaj alındı message info title + + 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 concurrency - Eşzamanlılık alınıyor - No comment provided by engineer. - Receiving file will be stopped. Dosya alımı durdurulacaktır. @@ -4584,11 +6075,36 @@ Hata: %@ 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ı? @@ -4609,10 +6125,23 @@ Hata: %@ Azaltılmış pil kullanımı No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject Reddet - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4626,12 +6155,12 @@ Hata: %@ Relay server is only used if necessary. Another party can observe your IP address. - Aktarma sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir. + 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. - Aktarıcı sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. + Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. No comment provided by engineer. @@ -4639,6 +6168,16 @@ Hata: %@ 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 @@ -4699,6 +6238,46 @@ Hata: %@ Yanıtla chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required Gerekli @@ -4709,16 +6288,41 @@ Hata: %@ 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 @@ -4759,9 +6363,8 @@ Hata: %@ Göster chat item action - - Revert - Geri al + + Review conditions No comment provided by engineer. @@ -4789,9 +6392,19 @@ Hata: %@ Sohbeti çalıştır No comment provided by engineer. - - SMP servers - SMP sunucuları + + SMP server + SMP sunucusu + No comment provided by engineer. + + + SOCKS proxy + SOCKS vekili + No comment provided by engineer. + + + Safely receive files + Dosyaları güvenle alın No comment provided by engineer. @@ -4802,43 +6415,43 @@ Hata: %@ Save Kaydet - chat item action + alert button +chat item action Save (and notify contacts) Kaydet (ve kişilere bildir) - No comment provided by engineer. + alert button Save and notify contact Kaydet ve kişilere bildir - No comment provided by engineer. + alert button 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 archive - Arşivi kaydet - No comment provided by engineer. - - - Save auto-accept settings - Otomatik kabul et ayarlarını kaydet - No comment provided by engineer. - Save group profile Grup profilini kaydet No comment provided by engineer. + + Save list + No comment provided by engineer. + Save passphrase and open chat Parolayı kaydet ve sohbeti aç @@ -4852,7 +6465,7 @@ Hata: %@ Save preferences? Tercihler kaydedilsin mi? - No comment provided by engineer. + alert title Save profile password @@ -4867,18 +6480,18 @@ Hata: %@ Save servers? Sunucular kaydedilsin mi? - No comment provided by engineer. - - - Save settings? - Ayarlar kaydedilsin mi? - No comment provided by engineer. + alert title Save welcome message? Hoşgeldin mesajı kaydedilsin mi? No comment provided by engineer. + + Save your profile? + Profiliniz kaydedilsin mi? + alert title + Saved Kaydedildi @@ -4899,6 +6512,21 @@ Hata: %@ Kaydedilmiş mesaj message info title + + 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 QR code QR kodu okut @@ -4939,11 +6567,21 @@ Hata: %@ Ara veya SimpleX bağlantısını yapıştır No comment provided by engineer. + + Secondary + İkincil renk + No comment provided by engineer. + Secure queue Sırayı koru server test step + + Secured + Güvenli + No comment provided by engineer. + Security assessment Güvenlik değerlendirmesi @@ -4957,6 +6595,21 @@ Hata: %@ Select Seç + chat item action + + + 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. @@ -4994,11 +6647,6 @@ Hata: %@ 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 @@ -5009,6 +6657,11 @@ Hata: %@ 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 @@ -5019,14 +6672,28 @@ Hata: %@ 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 notifications: - Bildirimler gönder: + + Send private reports No comment provided by engineer. @@ -5052,7 +6719,7 @@ Hata: %@ Sender cancelled file transfer. Gönderici dosya gönderimini iptal etti. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5066,7 +6733,7 @@ Hata: %@ Sending delivery receipts will be enabled for all contacts. - Gönderildi bilgisi tüm kişiler için etkinleştirilecektir. + Tüm kişiler için iletim bilgisi gönderme özelliği etkinleştirilecek. No comment provided by engineer. @@ -5109,6 +6776,11 @@ Hata: %@ Şuradan gönderildi: %@ copied message info + + Sent directly + Direkt gönderildi + No comment provided by engineer. + Sent file event Dosya etkinliği gönderildi @@ -5119,11 +6791,67 @@ Hata: %@ Mesaj gönderildi message info title + + 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 added to operator %@. + alert message + + + Server address + Sunucu adresi + No comment provided by engineer. + + + Server address is incompatible with network settings. + Sunucu adresi ağ ayarlarıyla uyumlu değil. + srv error text. + + + Server address is incompatible with network settings: %@. + Sunucu adresi ağ ayarlarıyla uyumsuz: %@. + No comment provided by engineer. + + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin @@ -5139,11 +6867,36 @@ Hata: %@ Sunucu testinde hata oluştu! No comment provided by engineer. + + Server type + Sunucu tipi + No comment provided by engineer. + + + Server version is incompatible with network settings. + Sunucu sürümü ağ ayarlarıyla uyumlu değil. + srv error text + + + 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 @@ -5154,11 +6907,20 @@ Hata: %@ 1 günlüğüne ayarla No comment provided by engineer. + + Set chat name… + 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 @@ -5169,6 +6931,10 @@ Hata: %@ Sistem kimlik doğrulaması yerine ayarla. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Şifre ayarla @@ -5199,6 +6965,11 @@ Hata: %@ Ayarlar No comment provided by engineer. + + Settings were changed. + Ayarlar değiştirildi. + alert message + Shape profile images Profil resimlerini şekillendir @@ -5207,21 +6978,39 @@ Hata: %@ Share Paylaş - chat item action + alert action +chat item action Share 1-time link Tek kullanımlık bağlantıyı paylaş No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Adresi paylaş No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Kişilerle adres paylaşılsın mı? + alert title + + + Share from other apps. + Diğer uygulamalardan paylaşın. No comment provided by engineer. @@ -5229,16 +7018,30 @@ Hata: %@ 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. + + Short link + No comment provided by engineer. + Show QR code QR kodunu göster @@ -5259,21 +7062,45 @@ Hata: %@ 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 → on messages sent via private routing. + Gizli yönlendirme yoluyla gönderilen mesajlarda → işaretini 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 Chat and Flux made an agreement to include Flux-operated servers into the app. + 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. @@ -5304,6 +7131,18 @@ Hata: %@ SimpleX adresi No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + + + SimpleX channel link + simplex link type + SimpleX contact address SimpleX kişi adresi @@ -5324,8 +7163,8 @@ Hata: %@ SimpleX bağlantıları chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. SimpleX bağlantıları bu grupta yasaklandı. No comment provided by engineer. @@ -5339,11 +7178,21 @@ Hata: %@ SimpleX tek kullanımlık davet simplex link type + + 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 @@ -5359,16 +7208,46 @@ Hata: %@ Küçük gruplar (en fazla 20 kişi) No comment provided by engineer. + + Soft + Yumuşak + blur media + + + 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: + No comment provided by engineer. + + + Some servers failed the test: +%@ + alert message + Somebody Biri notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. Kare,daire, veya aralarında herhangi bir şey. @@ -5389,6 +7268,16 @@ Hata: %@ Geçişi başlat No comment provided by engineer. + + Starting from %@. + %@'dan başlayarak. + No comment provided by engineer. + + + Statistics + İstatistikler + No comment provided by engineer. + Stop Dur @@ -5404,11 +7293,6 @@ Hata: %@ Sohbeti kes No comment provided by engineer. - - Stop chat to enable database actions - Veritabanı eylemlerini etkinleştirmek için sohbeti durdur - 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. @@ -5437,28 +7321,62 @@ Hata: %@ Stop sharing Paylaşmayı durdur - No comment provided by engineer. + alert action Stop sharing address? Adresi paylaşmak durdurulsun mu? - No comment provided by engineer. + alert title Stopping chat Sohbeti durdurma No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + Güçlü + blur media + 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 @@ -5469,11 +7387,20 @@ Hata: %@ Sistem yetkilendirilmesi 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 port for messaging + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5489,11 +7416,20 @@ Hata: %@ TCP_TVLDEKAL 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 Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Tuşa bas @@ -5529,16 +7465,20 @@ Hata: %@ Taramak için tıkla No comment provided by engineer. - - Tap to start a new chat - Yeni bir sohbet başlatmak için tıkla - No comment provided by engineer. + + Temporary file error + Geçici dosya hatası + file error alert title Test failed at step %@. Test %@ adımında başarısız oldu. server test failure + + Test notifications + No comment provided by engineer. + Test server Sunucuyu test et @@ -5552,7 +7492,7 @@ Hata: %@ Tests failed! Testler başarısız oldu! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5569,11 +7509,6 @@ Hata: %@ Kullanıcılar için teşekkürler - Weblate aracılığıyla katkıda bulun! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Herhangi bir kullanıcı tanımlayıcısı olmayan ilk platform - tasarım gereği gizli. - No comment provided by engineer. - 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. @@ -5586,6 +7521,15 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. 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 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). + 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ı. @@ -5596,6 +7540,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Taradığınız kod bir SimpleX bağlantı QR kodu değildir. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Bağlantı kabulünüz iptal edilecektir! @@ -5616,6 +7564,11 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Şifreleme çalışıyor ve yeni şifreleme anlaşması gerekli değil. Bağlantı hatalarına neden olabilir! No comment provided by engineer. + + The future of messaging + Gizli mesajlaşmanın yeni nesli + No comment provided by engineer. + The hash of the previous message is different. Önceki mesajın hash'i farklı. @@ -5631,9 +7584,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Mesaj tüm üyeler için yönetilmiş olarak işaretlenecektir. No comment provided by engineer. - - The next generation of private messaging - Gizli mesajlaşmanın yeni nesli + + 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. @@ -5641,9 +7599,12 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir. No comment provided by engineer. - - The profile is only shared with your contacts. - Profil sadece kişilerinle paylaşılacak. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! No comment provided by engineer. @@ -5661,14 +7622,27 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Mevcut sohbet profilinizin yeni bağlantıları için sunucular **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + 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. - - Theme - Tema + + 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 conditions will also apply for: **%@**. No comment provided by engineer. @@ -5691,6 +7665,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. 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 - the messages sent and received in this chat earlier than selected will be deleted. + alert message + 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. @@ -5736,11 +7714,29 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu senin kendi tek kullanımlık bağlantın! 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. + 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 message was deleted or not received yet. + 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: @@ -5761,9 +7757,8 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Yeni bir bağlantı oluşturmak için 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. - 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. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -5771,6 +7766,11 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Zaman bölgesini korumak için,fotoğraf/ses dosyaları UTC kullanır. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5778,6 +7778,25 @@ You will be prompted to complete authentication before this feature is enabled.< Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenecektir. 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 receive + 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. @@ -5788,26 +7807,58 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Gizli profilinizi ortaya çıkarmak için **Sohbet profilleriniz** sayfasındaki arama alanına tam bir şifre girin. No comment provided by engineer. + + To send + 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 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. 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. + + Token status: %@. + token status + + + 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: %@). @@ -5863,10 +7914,9 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Üyenin engeli kaldırılsın mı? No comment provided by engineer. - - Unexpected error: %@ - Beklenmeyen hata: %@ - item status description + + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5876,7 +7926,7 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Unfav. Favorilerden çık. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +7963,11 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Bilinmeyen hata No comment provided by engineer. + + Unknown servers! + Bilinmeyen sunucular! + alert title + 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. @@ -5948,11 +8003,15 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Unmute Susturmayı kaldır - No comment provided by engineer. + notification label action Unread Okunmamış + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5965,11 +8024,6 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Güncelle No comment provided by engineer. - - Update .onion hosts setting? - .onion ana bilgisayarların ayarı güncellensin mi? - No comment provided by engineer. - Update database passphrase Veritabanı parolasını güncelle @@ -5980,9 +8034,13 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Bağlantı ayarları güncellensin mi? No comment provided by engineer. - - Update transport isolation mode? - Taşıma izolasyon modu güncellensin mi? + + Update settings? + Ayarları güncelleyelim mi? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5990,16 +8048,16 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Ayarların güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Bu ayarı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 @@ -6010,21 +8068,48 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Dosya yükle server test step + + 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 %@ + No comment provided by engineer. + Use .onion hosts .onion ana bilgisayarlarını kullan No comment provided by engineer. + + Use SOCKS proxy + SOCKS vekili kullan + No comment provided by engineer. + Use SimpleX Chat servers? SimpleX Chat sunucuları kullanılsın mı? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Sohbeti kullan @@ -6035,6 +8120,14 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Şu anki profili kullan 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 Yeni bağlantılar için kullan @@ -6060,24 +8153,51 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste 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 servers + No comment provided by engineer. + + + Use short links (BETA) + 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. - - User profile - Kullanıcı profili + + Use the app with one hand. + Uygulamayı tek elle kullan. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - .onion ana bilgisayarlarını kullanmak için uyumlu VPN sağlayıcısı gerekir. + + Use web port + 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. @@ -6150,11 +8270,19 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste 1gb'a kadar videolar ve dosyalar No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Güvenlik kodunu görüntüle No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Görünür geçmiş @@ -6170,8 +8298,8 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Bu sohbette sesli mesajlar yasaktır. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Bu grupta sesli mesajlar yasaktır. No comment provided by engineer. @@ -6210,6 +8338,16 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste 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. + 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 @@ -6255,9 +8393,8 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Sesli ve görüntülü aramalara bağlanırken. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - İnsanlar bağlantı talebinde bulunduğunda, kabul edebilir veya reddedebilirsiniz. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -6295,24 +8432,39 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste 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. + No comment provided by engineer. + + + 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: %@. + alert message + Wrong database passphrase Yanlış veritabanı parolası No comment provided by engineer. + + 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. + snd 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. + file error text + Wrong passphrase! Yanlış parola! No comment provided by engineer. - - XFTP servers - XFTP sunucuları - No comment provided by engineer. - - - You - Sen + + XFTP server + XFTP sunucusu No comment provided by engineer. @@ -6340,6 +8492,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Zaten %@'a bağlısınız. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Zaten %@'a bağlanıyorsunuz. @@ -6387,11 +8543,25 @@ Katılma isteği tekrarlansın mı? 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 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 configure servers via settings. + No comment provided by engineer. + You can create it later Daha sonra oluşturabilirsiniz @@ -6422,11 +8592,20 @@ Katılma isteği tekrarlansın mı? Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Artık %@ adresine mesaj gönderebilirsin notification body + + You can send messages to %@ from Archived contacts. + Arşivlenen kişilerden %@'ya mesaj gönderebilirsiniz. + 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. Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz. @@ -6442,16 +8621,16 @@ Katılma isteği tekrarlansın mı? Bu adresi kişilerinizle paylaşarak onların **%@** ile bağlantı kurmasını sağlayabilirsiniz. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Adresinizi bir bağlantı veya QR kodu olarak paylaşabilirsiniz - herkes size bağlanabilir. - 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. @@ -6465,23 +8644,23 @@ Katılma isteği tekrarlansın mı? 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. + alert message You can't send messages! Mesajlar gönderemezsiniz! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Mesajların hangi sunucu(lar)dan **alınacağını**, kişilerinizi - onlara mesaj göndermek için kullandığınız sunucuları - siz kontrol edersiniz. - 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! @@ -6494,11 +8673,6 @@ Repeat connection request? Bağlantı isteği tekrarlansın mı? No comment provided by engineer. - - You have no chats - Hiç sohbetiniz yok - 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. @@ -6519,11 +8693,26 @@ Bağlantı isteği tekrarlansın mı? Bu gruba katıldınız. Davet eden grup üyesine bağlanılıyor. 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. @@ -6539,6 +8728,10 @@ Bağlantı isteği tekrarlansın mı? Grup daveti gönderdiniz No comment provided by engineer. + + You should receive notifications. + token info + 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! @@ -6574,6 +8767,10 @@ Bağlantı isteği tekrarlansın mı? Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + 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. @@ -6594,31 +8791,16 @@ Bağlantı isteği tekrarlansın mı? 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 %@ servers - %@ sunucularınız - No comment provided by engineer. - Your ICE servers ICE sunucularınız No comment provided by engineer. - - Your SMP servers - SMP sunucularınız - No comment provided by engineer. - Your SimpleX address SimpleX adresin No comment provided by engineer. - - Your XFTP servers - XFTP sunucularınız - No comment provided by engineer. - Your calls Aramaların @@ -6634,16 +8816,19 @@ Bağlantı isteği tekrarlansın mı? Sohbet veritabanınız şifrelenmemiş - şifrelemek için parola ayarlayın. No comment provided by engineer. + + Your chat preferences + Sohbet tercihleriniz + alert title + Your chat profiles Sohbet profillerin No comment provided by engineer. - - 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). - Bağlantının tamamlanması için kişinizin çevrimiçi olması gerekir. -Bu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yeni bir bağlantıyla deneyebilirsiniz). + + 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. @@ -6661,6 +8846,11 @@ Bu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yen 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. @@ -6691,33 +8881,35 @@ Bu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yen 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. + + Your profile is stored on your device and only shared with your contacts. + Profil sadece kişilerinle paylaşılacak. 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. + + 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. No comment provided by engineer. + + 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. + alert message + Your random profile Rasgele profiliniz No comment provided by engineer. - - Your server - Sunucunuz - No comment provided by engineer. - Your server address Sunucu adresiniz No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Ayarlarınız @@ -6758,6 +8950,10 @@ SimpleX sunucuları profilinizi göremez. kabul edilen arama call status + + accepted invitation + chat list item title + admin yönetici @@ -6793,6 +8989,15 @@ SimpleX sunucuları profilinizi göremez. ve %lld diğer etkinlikler No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + denemeler + No comment provided by engineer. + audio call (not e2e encrypted) sesli arama (uçtan uca şifreli değil) @@ -6826,13 +9031,19 @@ SimpleX sunucuları profilinizi göremez. blocked by admin yönetici tarafından engellendi - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold kalın No comment provided by engineer. + + call + Ara + No comment provided by engineer. + call error arama hatası @@ -6936,7 +9147,7 @@ SimpleX sunucuları profilinizi göremez. connecting… bağlanılıyor… - chat list item title + No comment provided by engineer. connection established @@ -6983,10 +9194,16 @@ SimpleX sunucuları profilinizi göremez. gün time unit + + decryption errors + Şifre çözme hataları + No comment provided by engineer. + default (%@) varsayılan (%@) - pref value + delete after time +pref value default (no) @@ -7033,6 +9250,11 @@ SimpleX sunucuları profilinizi göremez. yinelenen mesaj integrity error chat item + + duplicates + Kopyalar + No comment provided by engineer. + e2e encrypted uçtan uca şifrelenmiş @@ -7108,9 +9330,9 @@ SimpleX sunucuları profilinizi göremez. hata No comment provided by engineer. - - event happened - etkinlik yaşandı + + expired + Süresi dolmuş No comment provided by engineer. @@ -7143,6 +9365,11 @@ SimpleX sunucuları profilinizi göremez. 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. + + inactive + inaktif + No comment provided by engineer. + incognito via contact address link kişi bağlantı linki aracılığıyla gizli @@ -7183,6 +9410,11 @@ SimpleX sunucuları profilinizi göremez. %@ grubuna davet group name + + invite + davet + No comment provided by engineer. + invited davet edildi @@ -7238,6 +9470,11 @@ SimpleX sunucuları profilinizi göremez. bağlanıldı rcv group event chat item + + message + mesaj + No comment provided by engineer. + message received mesaj alındı @@ -7263,6 +9500,10 @@ SimpleX sunucuları profilinizi göremez. %@ tarafından yönetilmekte marked deleted chat item preview text + + moderator + member role + months aylar @@ -7271,7 +9512,7 @@ SimpleX sunucuları profilinizi göremez. never asla - No comment provided by engineer. + delete after time new message @@ -7302,8 +9543,8 @@ SimpleX sunucuları profilinizi göremez. off kapalı enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7320,6 +9561,16 @@ SimpleX sunucuları profilinizi göremez. açık group pref value + + other + diğer + No comment provided by engineer. + + + other errors + diğer hatalar + No comment provided by engineer. + owner sahip @@ -7335,6 +9586,14 @@ SimpleX sunucuları profilinizi göremez. eşler arası No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption kuantuma dayanıklı e2e şifreleme @@ -7350,6 +9609,10 @@ SimpleX sunucuları profilinizi göremez. onaylama alındı… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call geri çevrilmiş çağrı @@ -7380,6 +9643,10 @@ SimpleX sunucuları profilinizi göremez. sen kaldırıldın rcv group event chat item + + requested to connect + chat list item title + saved kaydedildi @@ -7390,6 +9657,11 @@ SimpleX sunucuları profilinizi göremez. %@ tarafından kaydedildi No comment provided by engineer. + + search + ara + No comment provided by engineer. + sec sn @@ -7415,6 +9687,15 @@ SimpleX sunucuları profilinizi göremez. doğrudan mesaj gönder No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + sunucu kuyruk bilgisi: %1$@ + +son alınan msj: %2$@ + queue info + set new contact address yeni kişi adresi ayarla @@ -7455,11 +9736,21 @@ SimpleX sunucuları profilinizi göremez. bilinmeyen connection info + + unknown servers + bilinmeyen yönlendiriciler + No comment provided by engineer. + unknown status bilinmeyen durum No comment provided by engineer. + + unprotected + korumasız + No comment provided by engineer. + updated group profile grup profili güncellendi @@ -7500,6 +9791,11 @@ SimpleX sunucuları profilinizi göremez. yönlendirici aracılığıyla No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) Görüntülü arama (şifrelenmiş değil) @@ -7525,6 +9821,11 @@ SimpleX sunucuları profilinizi göremez. haftalar time unit + + when IP hidden + IP gizliyken + No comment provided by engineer. + yes evet @@ -7609,7 +9910,7 @@ SimpleX sunucuları profilinizi göremez.
- +
@@ -7646,7 +9947,7 @@ SimpleX sunucuları profilinizi göremez.
- +
@@ -7666,4 +9967,245 @@ SimpleX sunucuları profilinizi göremez.
+ +
+ +
+ + + %d new events + notification body + + + From %d chat(s) + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Telif Hakkı © 2024 SimpleX Chat. Tüm hakları saklıdır. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 index 124ddbcc33..9c675514f4 100644 --- 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 @@ -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/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 index cf485752ea..cb83427195 100644 --- 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 @@ -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/tr.xcloc/contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json index 0aee97a599..2e32ea2080 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "tr", "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/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 1ed1f5ffd6..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 @@
- +
- - - - - - 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) (можна скопіювати) @@ -127,6 +100,11 @@ %@ перевірено No comment provided by engineer. + + %@ server + %@ сервер + No comment provided by engineer. + %@ servers %@ сервери @@ -142,6 +120,11 @@ %@ хоче підключитися! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ та %lld учасників @@ -162,11 +145,36 @@ %d днів time interval + + %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 файл(и) не було завантажено. + forward confirmation reason + %d hours %d годин time interval + + %d messages not forwarded + %d повідомлень не переслано + alert title + %d min %d хв @@ -182,6 +190,10 @@ %d сек time interval + + %d seconds(s) + delete after time + %d skipped message(s) %d пропущено повідомлення(ь) @@ -252,11 +264,6 @@ %lld нові мови інтерфейсу No comment provided by engineer. - - %lld second(s) - %lld секунд(и) - No comment provided by engineer. - %lld seconds %lld секунд @@ -307,11 +314,6 @@ %u повідомлень пропущено. No comment provided by engineer. - - ( - ( - No comment provided by engineer. - (new) (новий) @@ -322,19 +324,9 @@ (цей пристрій v%@) No comment provided by engineer. - - ) - ) - No comment provided by engineer. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Додати контакт**: створити нове посилання-запрошення або підключитися за отриманим посиланням. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту. + + **Create 1-time link**: to create and share a new invitation link. + **Додати контакт**: створити нове посилання-запрошення. No comment provided by engineer. @@ -342,13 +334,13 @@ **Створити групу**: створити нову групу. 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. + + **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). + + **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. @@ -362,11 +354,16 @@ **Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його. 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. + + **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 / Paste link**: to connect via a link you received. + **Відсканувати / Вставити посилання**: підключитися за отриманим посиланням. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку. @@ -392,11 +389,6 @@ \*жирний* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -433,11 +425,6 @@ - історія редагування. No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec 0 сек @@ -451,7 +438,8 @@ 1 day 1 день - time interval + delete after time +time interval 1 hour @@ -466,12 +454,28 @@ 1 month 1 місяць - time interval + delete after time +time interval 1 week 1 тиждень - time interval + delete after time +time interval + + + 1 year + delete after time + + + 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 @@ -488,11 +492,6 @@ 30 секунд No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -542,31 +541,32 @@ Скасувати зміну адреси? No comment provided by engineer. - - About SimpleX - Про SimpleX - No comment provided by engineer. - About SimpleX Chat Про чат SimpleX No comment provided by engineer. - - About SimpleX address - Про адресу SimpleX + + About operators + Про операторів No comment provided by engineer. - - Accent color - Акцентний колір + + Accent + Акцент No comment provided by engineer. Accept Прийняти accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + Прийняти умови + No comment provided by engineer. Accept connection request? @@ -581,21 +581,45 @@ Accept incognito Прийняти інкогніто - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + Прийняті умови + No comment provided by engineer. + + + Acknowledged + Визнано + No comment provided by engineer. + + + Acknowledgement errors + Помилки підтвердження + No comment provided by engineer. + + + Active + token status text + + + 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 contact - Додати контакт + + Add friends + Додайте друзів No comment provided by engineer. - - Add preset servers - Додавання попередньо встановлених серверів + + Add list No comment provided by engineer. @@ -603,14 +627,19 @@ Додати профіль No comment provided by engineer. + + Add server + Додати сервер + No comment provided by engineer. + Add servers by scanning QR codes. Додайте сервери, відсканувавши QR-код. No comment provided by engineer. - - Add server… - Додати сервер… + + Add team members + Додайте учасників команди No comment provided by engineer. @@ -618,11 +647,45 @@ Додати до іншого пристрою 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 Адреса @@ -633,6 +696,16 @@ Зміна адреси буде скасована. Буде використано стару адресу отримання. No comment provided by engineer. + + Address or 1-time link? + Адреса чи одноразове посилання? + No comment provided by engineer. + + + Address settings + Налаштування адреси + No comment provided by engineer. + Admins can block a member for all. Адміністратори можуть заблокувати користувача для всіх. @@ -648,6 +721,15 @@ Розширені налаштування мережі No comment provided by engineer. + + Advanced settings + Додаткові налаштування + No comment provided by engineer. + + + All + No comment provided by engineer. + All app data is deleted. Всі дані програми видаляються. @@ -658,16 +740,30 @@ Всі чати та повідомлення будуть видалені - це неможливо скасувати! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + alert message + 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. Всі учасники групи залишаться на зв'язку. 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! Усі повідомлення будуть видалені - цю дію не можна скасувати! @@ -683,6 +779,19 @@ Всі нові повідомлення від %@ будуть приховані! No comment provided by engineer. + + All profiles + Всі профілі + profile dropdown + + + 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. Всі ваші контакти залишаться на зв'язку. @@ -708,11 +817,21 @@ Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх. 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 downgrade + Дозволити пониження версії + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години) @@ -738,13 +857,23 @@ Дозволити надсилання зникаючих повідомлень. No comment provided by engineer. + + 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 SimpleX links. + Дозволити надсилати посилання SimpleX. No comment provided by engineer. @@ -807,6 +936,11 @@ Вже приєднуємося до групи! No comment provided by engineer. + + Always use private routing. + Завжди використовуйте приватну маршрутизацію. + No comment provided by engineer. + Always use relay Завжди використовуйте реле @@ -817,11 +951,20 @@ Створюється порожній профіль чату з вказаним ім'ям, і додаток відкривається у звичайному режимі. No comment provided by engineer. + + Another reason + report reason + Answer call Відповісти на дзвінок No comment provided by engineer. + + Anybody can host servers. + Кожен може хостити сервери. + No comment provided by engineer. + App build: %@ Збірка програми: %@ @@ -837,6 +980,10 @@ Додаток шифрує нові локальні файли (крім відео). No comment provided by engineer. + + App group: + No comment provided by engineer. + App icon Іконка програми @@ -852,6 +999,11 @@ Пароль програми замінено на пароль самознищення. No comment provided by engineer. + + App session + Сесія програми + No comment provided by engineer. + App version Версія програми @@ -872,11 +1024,50 @@ Подати заявку No comment provided by engineer. + + Apply to + Звертатися до + No comment provided by engineer. + + + Archive + No comment provided by engineer. + + + Archive %lld reports? + 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? + No comment provided by engineer. + + + Archive reports + swipe action + + + Archived contacts + Архівні контакти + No comment provided by engineer. + Archiving database Архівування бази даних @@ -942,11 +1133,21 @@ Автоматичне прийняття зображень No comment provided by engineer. + + Auto-accept settings + Автоприйняття налаштувань + alert title + Back Назад No comment provided by engineer. + + Background + Фон + No comment provided by engineer. + Bad desktop address Неправильна адреса робочого столу @@ -962,16 +1163,59 @@ Поганий хеш повідомлення 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 Блокувати @@ -1007,6 +1251,16 @@ Заблокований адміністратором No comment provided by engineer. + + Blur for better privacy. + Розмиття для кращої приватності. + No comment provided by engineer. + + + Blur media + Розмиття медіа + No comment provided by engineer. + Both you and your contact can add message reactions. Реакції на повідомлення можете додавати як ви, так і ваш контакт. @@ -1037,11 +1291,31 @@ Болгарською, фінською, тайською та українською мовами - завдяки користувачам та [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: +- send only legal content in public groups. +- respect other users – no spam. + No comment provided by engineer. + Call already ended! Дзвінок вже закінчився! @@ -1052,11 +1326,26 @@ Дзвінки No comment provided by engineer. + + 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! Не вдається запросити контакт! @@ -1067,10 +1356,16 @@ Неможливо запросити контакти! No comment provided by engineer. + + Can't message member + Не можу надіслати повідомлення користувачеві + No comment provided by engineer. + Cancel Скасувати - No comment provided by engineer. + alert action +alert button Cancel migration @@ -1082,13 +1377,24 @@ Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних No comment provided by engineer. + + Cannot forward message + Неможливо переслати повідомлення + No comment provided by engineer. + Cannot receive file Не вдається отримати файл - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + Перевищено ліміт - одержувач не отримав раніше надіслані повідомлення. + snd error text Cellular + Стільниковий No comment provided by engineer. @@ -1096,6 +1402,15 @@ Зміна No comment provided by engineer. + + Change automatic message deletion? + alert title + + + Change chat profiles + Зміна профілів користувачів + authentication reason + Change database passphrase? Змінити пароль до бази даних? @@ -1140,11 +1455,26 @@ Change self-destruct passcode Змінити пароль самознищення authentication reason - set passcode view +set passcode view - - 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. @@ -1162,6 +1492,11 @@ Видалено базу даних чату No comment provided by engineer. + + Chat database exported + Експортовано базу даних чату + No comment provided by engineer. + Chat database imported Імпорт бази даних чату @@ -1182,6 +1517,11 @@ Чат зупинено. Якщо ви вже використовували цю базу даних на іншому пристрої, перенесіть її назад перед запуском чату. No comment provided by engineer. + + Chat list + Список чатів + No comment provided by engineer. + Chat migrated! Чат перемістився! @@ -1192,15 +1532,50 @@ Налаштування чату No comment provided by engineer. + + Chat preferences were changed. + Змінено налаштування чату. + alert message + + + 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. + Перевірте повідомлення, коли це дозволено. + No comment provided by engineer. + Check server address and try again. Перевірте адресу сервера та спробуйте ще раз. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1222,10 +1597,25 @@ Виберіть з бібліотеки No comment provided by engineer. + + Chunks deleted + Фрагменти видалено + No comment provided by engineer. + + + Chunks downloaded + Завантажено фрагменти + No comment provided by engineer. + + + Chunks uploaded + Завантажено фрагменти + No comment provided by engineer. + Clear Чисто - No comment provided by engineer. + swipe action Clear conversation @@ -1237,6 +1627,14 @@ Відверта розмова? No comment provided by engineer. + + Clear group? + No comment provided by engineer. + + + Clear or delete group? + No comment provided by engineer. + Clear private notes? Чисті приватні нотатки? @@ -1247,11 +1645,20 @@ Очистити перевірку No comment provided by engineer. - - Colors - Кольори + + Color chats with the new themes. + Кольорові чати з новими темами. No comment provided by engineer. + + Color mode + Колірний режим + No comment provided by engineer. + + + Community guidelines violation + report reason + Compare file Порівняти файл @@ -1262,11 +1669,55 @@ Порівняйте коди безпеки зі своїми контактами. 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 Підтвердити @@ -1277,11 +1728,21 @@ Підтвердити пароль 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 Підтвердьте налаштування мережі @@ -1307,6 +1768,10 @@ Підтвердити завантаження No comment provided by engineer. + + Confirmed + token status text + Connect Підключіться @@ -1327,6 +1792,11 @@ Підключення до комп'ютера No comment provided by engineer. + + Connect to your friends faster. + Швидше спілкуйтеся з друзями. + No comment provided by engineer. + Connect to yourself? З'єднатися з самим собою? @@ -1366,16 +1836,31 @@ This is your own one-time link! Підключитися до %@ 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 to server… Підключення до сервера… @@ -1386,6 +1871,11 @@ This is your own one-time link! Підключення до сервера... (помилка: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + З'єднання з контактом, будь ласка, зачекайте або перевірте пізніше! + No comment provided by engineer. + Connecting to desktop Підключення до ПК @@ -1396,6 +1886,15 @@ This is your own one-time link! Підключення No comment provided by engineer. + + Connection and servers status. + Стан з'єднання та серверів. + No comment provided by engineer. + + + Connection blocked + No comment provided by engineer. + Connection error Помилка підключення @@ -1406,11 +1905,34 @@ This is your own one-time link! Помилка підключення (AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + 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 З'єднання розірвано @@ -1421,6 +1943,16 @@ This is your own one-time link! Тайм-аут з'єднання No comment provided by engineer. + + Connection with desktop stopped + Припинено зв'язок з робочим столом + No comment provided by engineer. + + + Connections + З'єднання + No comment provided by engineer. + Contact allows Контакт дозволяє @@ -1431,6 +1963,11 @@ This is your own one-time link! Контакт вже існує No comment provided by engineer. + + Contact deleted! + Контакт видалено! + No comment provided by engineer. + Contact hidden: Контакт приховано: @@ -1441,9 +1978,9 @@ This is your own one-time link! Контакт підключений notification - - Contact is not connected yet! - Контакт ще не підключено! + + Contact is deleted. + Контакт видалено. No comment provided by engineer. @@ -1456,6 +1993,11 @@ This is your own one-time link! Налаштування контактів No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Контакт буде видалено - це неможливо скасувати! + No comment provided by engineer. + Contacts Контакти @@ -1466,21 +2008,40 @@ This is your own one-time link! Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути. No comment provided by engineer. + + Content violates conditions of use + blocking reason + Continue Продовжуйте No comment provided by engineer. + + Conversation deleted! + Розмова видалена! + No comment provided by engineer. + Copy Копіювати - chat item action + 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 %@? Виправити ім'я на %@? @@ -1491,6 +2052,11 @@ This is your own one-time link! Створити No comment provided by engineer. + + Create 1-time link + Створити одноразове посилання + No comment provided by engineer. + Create SimpleX address Створіть адресу SimpleX @@ -1501,11 +2067,6 @@ This is your own one-time link! Створіть групу, використовуючи випадковий профіль. No comment provided by engineer. - - Create an address to let people connect with you. - Створіть адресу, щоб люди могли з вами зв'язатися. - No comment provided by engineer. - Create file Створити файл @@ -1526,6 +2087,10 @@ This is your own one-time link! Створити посилання No comment provided by engineer. + + Create list + No comment provided by engineer. + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 Створіть новий профіль у [desktop app](https://simplex.chat/downloads/). 💻 @@ -1551,6 +2116,11 @@ This is your own one-time link! Створіть свій профіль No comment provided by engineer. + + Created + Створено + No comment provided by engineer. + Created at Створено за адресою @@ -1561,11 +2131,6 @@ This is your own one-time link! Створено за адресою: %@ copied message info - - Created on %@ - Створено %@ - No comment provided by engineer. - Creating archive link Створення архівного посилання @@ -1581,11 +2146,21 @@ This is your own one-time link! Поточний пароль 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 passphrase… Поточна парольна фраза… No comment provided by engineer. + + Current profile + Поточний профіль + No comment provided by engineer. + Currently maximum supported file size is %@. Наразі максимальний підтримуваний розмір файлу - %@. @@ -1596,11 +2171,26 @@ This is your own one-time link! Індивідуальний час 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 ID Ідентифікатор бази даних @@ -1699,6 +2289,11 @@ This is your own one-time link! База даних буде перенесена під час перезапуску програми No comment provided by engineer. + + Debug delivery + Доставка налагодження + No comment provided by engineer. + Decentralized Децентралізований @@ -1712,18 +2307,19 @@ This is your own one-time link! Delete Видалити - chat item action + alert action +swipe action + + + Delete %lld messages of members? + Видалити %lld повідомлень користувачів? + No comment provided by engineer. Delete %lld messages? Видалити %lld повідомлень? No comment provided by engineer. - - Delete Contact - Видалити контакт - No comment provided by engineer. - Delete address Видалити адресу @@ -1749,14 +2345,13 @@ This is your own one-time link! Видалити та повідомити контакт 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. @@ -1769,6 +2364,11 @@ This is your own one-time link! Видалити профіль чату? No comment provided by engineer. + + Delete chat? + Видалити чат? + No comment provided by engineer. + Delete connection Видалити підключення @@ -1779,11 +2379,9 @@ This is your own one-time link! Видалити контакт No comment provided by engineer. - - Delete contact? -This cannot be undone! - Видалити контакт? -Це не можна скасувати! + + Delete contact? + Видалити контакт? No comment provided by engineer. @@ -1846,6 +2444,10 @@ This cannot be undone! Видалити посилання? No comment provided by engineer. + + Delete list? + alert title + Delete member message? Видалити повідомлення учасника? @@ -1859,7 +2461,7 @@ This cannot be undone! Delete messages Видалити повідомлення - No comment provided by engineer. + alert button Delete messages after @@ -1876,9 +2478,9 @@ This cannot be undone! Видалити стару базу даних? No comment provided by engineer. - - Delete pending connection - Видалити очікуване з'єднання + + Delete or moderate up to 200 messages. + Видалити або модерувати до 200 повідомлень. No comment provided by engineer. @@ -1896,11 +2498,30 @@ This cannot be undone! Видалити чергу server test step + + 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 + Видалення без попередження + No comment provided by engineer. + + + Deleted + Видалено + No comment provided by engineer. + Deleted at Видалено за @@ -1911,6 +2532,16 @@ This cannot be undone! Видалено за: %@ copied message info + + Deletion errors + Помилки видалення + No comment provided by engineer. + + + Delivered even when Apple drops them. + Доставляються навіть тоді, коли Apple кидає їх. + No comment provided by engineer. + Delivery Доставка @@ -1946,11 +2577,41 @@ This cannot be undone! Настільні пристрої No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Адреса сервера призначення %@ несумісна з налаштуваннями сервера пересилання %@. + No comment provided by engineer. + + + Destination server error: %@ + Помилка сервера призначення: %@ + snd error text + + + 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 Інструменти для розробників @@ -1981,8 +2642,13 @@ This cannot be undone! Прямі повідомлення chat feature - - 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. @@ -1996,11 +2662,24 @@ This cannot be undone! Вимкнути SimpleX Lock authentication reason + + Disable automatic message deletion? + alert title + + + Disable delete messages + alert button + Disable for all Вимкнути для всіх No comment provided by engineer. + + Disabled + Вимкнено + No comment provided by engineer. + Disappearing message Зникаюче повідомлення @@ -2016,8 +2695,8 @@ This cannot be undone! Зникаючі повідомлення в цьому чаті заборонені. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. У цій групі заборонено зникаючі повідомлення. No comment provided by engineer. @@ -2051,11 +2730,21 @@ This cannot be undone! Відкриття через локальну мережу 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 SimpleX for emergency calls. НЕ використовуйте SimpleX для екстрених викликів. No comment provided by engineer. + + Do NOT use private routing. + НЕ використовуйте приватну маршрутизацію. + No comment provided by engineer. + Do it later Зробіть це пізніше @@ -2066,6 +2755,15 @@ This cannot be undone! Не надсилайте історію новим користувачам. No comment provided by engineer. + + Do not use credentials with proxy. + Не використовуйте облікові дані з проксі. + No comment provided by engineer. + + + Documents: + No comment provided by engineer. + Don't create address Не створювати адресу @@ -2076,11 +2774,19 @@ This cannot be undone! Не вмикати 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 Пониження та відкритий чат @@ -2088,7 +2794,14 @@ This cannot be undone! Download - chat item action + Завантажити + alert button +chat item action + + + Download errors + Помилки завантаження + No comment provided by engineer. Download failed @@ -2100,6 +2813,21 @@ This cannot be undone! Завантажити файл server test step + + Download files + Завантажити файли + alert action + + + Downloaded + Завантажено + No comment provided by engineer. + + + Downloaded files + Завантажені файли + No comment provided by engineer. + Downloading archive Завантажити архів @@ -2120,6 +2848,11 @@ This cannot be undone! Тривалість No comment provided by engineer. + + E2E encrypted notifications. + Зашифровані сповіщення E2E. + No comment provided by engineer. + Edit Редагувати @@ -2140,6 +2873,10 @@ This cannot be undone! Увімкнути (зберегти перевизначення) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + No comment provided by engineer. + Enable SimpleX Lock Увімкнути SimpleX Lock @@ -2153,7 +2890,7 @@ This cannot be undone! Enable automatic message deletion? Увімкнути автоматичне видалення повідомлень? - No comment provided by engineer. + alert title Enable camera access @@ -2200,8 +2937,14 @@ This cannot be undone! Увімкнути пароль самознищення set passcode view + + Enabled + Увімкнено + No comment provided by engineer. + Enabled for + Увімкнено для No comment provided by engineer. @@ -2274,6 +3017,10 @@ This cannot be undone! Повторне узгодження шифрування не вдалося. No comment provided by engineer. + + Encryption renegotiation in progress. + No comment provided by engineer. + Enter Passcode Введіть пароль @@ -2339,26 +3086,36 @@ This cannot be undone! Помилка скасування зміни адреси No comment provided by engineer. + + Error accepting conditions + Помилка прийняття умов + alert title + Error accepting contact request Помилка при прийнятті запиту на контакт No comment provided by engineer. - - Error accessing database file - Помилка доступу до файлу бази даних - No comment provided by engineer. - Error adding member(s) Помилка додавання користувача(ів) No comment provided by engineer. + + Error adding server + Помилка додавання сервера + alert title + Error changing address Помилка зміни адреси No comment provided by engineer. + + Error changing connection profile + Помилка при зміні профілю з'єднання + No comment provided by engineer. + Error changing role Помилка зміни ролі @@ -2369,6 +3126,20 @@ This cannot be undone! Помилка зміни налаштування 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 Помилка створення адреси @@ -2384,6 +3155,10 @@ This cannot be undone! Помилка створення посилання на групу No comment provided by engineer. + + Error creating list + alert title + Error creating member contact Помилка при створенні контакту користувача @@ -2399,6 +3174,10 @@ This cannot be undone! Помилка створення профілю! No comment provided by engineer. + + Error creating report + No comment provided by engineer. + Error decrypting file Помилка розшифрування файлу @@ -2419,11 +3198,6 @@ This cannot be undone! Помилка видалення з'єднання No comment provided by engineer. - - Error deleting contact - Помилка видалення контакту - No comment provided by engineer. - Error deleting database Помилка видалення бази даних @@ -2469,6 +3243,11 @@ This cannot be undone! Помилка експорту бази даних чату No comment provided by engineer. + + Error exporting theme: %@ + Помилка експорту теми: %@ + No comment provided by engineer. + Error importing chat database Помилка імпорту бази даних чату @@ -2479,9 +3258,14 @@ This cannot be undone! Помилка приєднання до групи No comment provided by engineer. - - Error loading %@ servers - Помилка завантаження %@ серверів + + Error loading servers + Помилка завантаження серверів + alert title + + + Error migrating settings + Помилка міграції налаштувань No comment provided by engineer. @@ -2492,16 +3276,34 @@ This cannot be undone! Error receiving file Помилка отримання файлу + alert title + + + Error reconnecting server + Помилка перепідключення сервера No comment provided by engineer. + + Error reconnecting servers + Помилка перепідключення серверів + No comment provided by engineer. + + + Error registering for notifications + alert title + Error removing member Помилка видалення учасника No comment provided by engineer. - - Error saving %@ servers - Помилка збереження %@ серверів + + Error reordering lists + alert title + + + Error resetting statistics + Статистика скидання помилок No comment provided by engineer. @@ -2509,6 +3311,10 @@ This cannot be undone! Помилка збереження серверів ICE No comment provided by engineer. + + Error saving chat list + alert title + Error saving group profile Помилка збереження профілю групи @@ -2524,6 +3330,11 @@ This cannot be undone! Помилка збереження пароля на keychain No comment provided by engineer. + + Error saving servers + Сервери збереження помилок + alert title + Error saving settings Налаштування збереження помилок @@ -2569,16 +3380,25 @@ This cannot be undone! Помилка зупинки чату No comment provided by engineer. + + Error switching profile + Помилка перемикання профілю + No comment provided by engineer. + Error switching profile! Помилка перемикання профілю! - No comment provided by engineer. + alertTitle Error synchronizing connection Помилка синхронізації з'єднання No comment provided by engineer. + + Error testing server connection + No comment provided by engineer. + Error updating group link Помилка оновлення посилання на групу @@ -2589,6 +3409,11 @@ This cannot be undone! Повідомлення про помилку оновлення No comment provided by engineer. + + Error updating server + Помилка оновлення сервера + alert title + Error updating settings Помилка оновлення налаштувань @@ -2617,7 +3442,9 @@ This cannot be undone! Error: %@ Помилка: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2629,6 +3456,16 @@ This cannot be undone! Помилка: немає файлу бази даних No comment provided by engineer. + + Errors + Помилки + No comment provided by engineer. + + + Errors in servers configuration. + Помилки в конфігурації серверів. + servers error + Even when disabled in the conversation. Навіть коли вимкнений у розмові. @@ -2644,6 +3481,10 @@ This cannot be undone! Розгорнути chat item action + + Expired + token status text + Export database Експорт бази даних @@ -2654,6 +3495,11 @@ This cannot be undone! Помилка експорту: No comment provided by engineer. + + Export theme + Тема експорту + No comment provided by engineer. + Exported database archive. Експортований архів бази даних. @@ -2679,16 +3525,65 @@ This cannot be undone! Швидко і без очікування, поки відправник буде онлайн! 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. + No comment provided by engineer. + Favorite Улюблений + swipe action + + + Favorites No comment provided by engineer. + + File error + Помилка файлу + file error alert title + + + File errors: +%@ + Помилки файлів: +%@ + alert message + + + File is blocked by server operator: +%@. + file error text + + + File not found - most likely file was deleted or cancelled. + Файл не знайдено - найімовірніше, файл було видалено або скасовано. + file error text + + + File server error: %@ + Помилка файлового сервера: %@ + file error text + + + File status + Статус файлу + No comment provided by engineer. + + + File status: %@ + Статус файлу: %@ + copied message info + File will be deleted from servers. Файл буде видалено з серверів. @@ -2709,6 +3604,11 @@ This cannot be undone! Файл: %@ No comment provided by engineer. + + Files + Файли + No comment provided by engineer. + Files & media Файли та медіа @@ -2719,13 +3619,14 @@ This cannot be undone! Файли і медіа chat feature - - 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. @@ -2788,27 +3689,113 @@ This cannot be undone! Виправлення не підтримується учасником групи No comment provided by engineer. + + For all moderators + No comment provided by engineer. + + + For chat profile %@: + Для профілю чату %@: + servers error + 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 + Для соціальних мереж + No comment provided by engineer. + Forward + Пересилання chat item action + + Forward %d message(s)? + Переслати %d повідомлення(ь)? + alert title + Forward and save messages + Пересилання та збереження повідомлень + No comment provided by engineer. + + + Forward messages + Пересилання повідомлень + alert action + + + Forward messages without files? + Пересилати повідомлення без файлів? + alert message + + + Forward up to 20 messages at once. + Пересилайте до 20 повідомлень одночасно. 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: %@. + Версія сервера переадресації несумісна з мережевими налаштуваннями: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Сервер переадресації: %1$@ +Помилка сервера призначення: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Сервер переадресації: %1$@ +Помилка: %2$@ + snd error text + Found desktop Знайдено робочий стіл @@ -2829,11 +3816,6 @@ This cannot be undone! Повне ім'я (необов'язково) No comment provided by engineer. - - Full name: - Повне ім'я: - No comment provided by engineer. - Fully decentralized – visible only to members. Повністю децентралізована - видима лише для учасників. @@ -2854,6 +3836,20 @@ This cannot be undone! GIF-файли та наклейки No comment provided by engineer. + + Get notified when mentioned. + No comment provided by engineer. + + + Good afternoon! + Доброго дня! + message preview + + + Good morning! + Доброго ранку! + message preview + Group Група @@ -2909,40 +3905,6 @@ This cannot be undone! Групові посилання No comment provided by engineer. - - Group members can add message reactions. - Учасники групи можуть додавати реакції на повідомлення. - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. (24 hours) - Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години) - No comment provided by engineer. - - - Group members can send SimpleX links. - 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. - Учасники групи можуть надсилати голосові повідомлення. - No comment provided by engineer. - Group message: Групове повідомлення: @@ -2983,11 +3945,19 @@ 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 Приховано @@ -3038,10 +4008,19 @@ This cannot be undone! Як працює SimpleX No comment provided by engineer. + + How it affects privacy + Як це впливає на конфіденційність + No comment provided by engineer. + + + How it helps privacy + Як це захищає приватність + No comment provided by engineer. + How it works - Як це працює - No comment provided by engineer. + alert button How to @@ -3068,6 +4047,11 @@ This cannot be undone! Сервери ICE (по одному на лінію) No comment provided by engineer. + + IP address + IP-адреса + No comment provided by engineer. + If you can't meet in person, show QR code in a video call, or share the link. Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням. @@ -3108,8 +4092,8 @@ This cannot be undone! Негайно No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Імунітет до спаму та зловживань No comment provided by engineer. @@ -3133,11 +4117,23 @@ This cannot be undone! Не вдалося імпортувати No comment provided by engineer. + + Import theme + Імпорт теми + No comment provided by engineer. + Importing archive Імпорт архіву No comment provided by engineer. + + Improved delivery, reduced traffic usage. +More improvements are coming soon! + Покращена доставка, зменшене використання трафіку. +Незабаром з'являться нові покращення! + No comment provided by engineer. + Improved message delivery Покращена доставка повідомлень @@ -3165,8 +4161,17 @@ This cannot be undone! In-call sounds + Звуки вхідного дзвінка No comment provided by engineer. + + Inappropriate content + report reason + + + Inappropriate profile + report reason + Incognito Інкогніто @@ -3237,6 +4242,11 @@ This cannot be undone! Встановіть [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! @@ -3244,16 +4254,36 @@ This cannot be undone! No comment provided by engineer. - - Instantly - Миттєво - No comment provided by engineer. - Interface Інтерфейс No comment provided by engineer. + + Interface colors + Кольори інтерфейсу + No comment provided by engineer. + + + Invalid + token status text + + + Invalid (bad token) + token status text + + + Invalid (expired) + token status text + + + Invalid (unregistered) + token status text + + + Invalid (wrong topic) + token status text + Invalid QR code Неправильний QR-код @@ -3292,7 +4322,7 @@ This cannot be undone! Invalid server address! Неправильна адреса сервера! - No comment provided by engineer. + alert title Invalid status @@ -3314,6 +4344,11 @@ This cannot be undone! Запросити учасників No comment provided by engineer. + + Invite to chat + Запросити в чат + No comment provided by engineer. + Invite to group Запросити до групи @@ -3329,8 +4364,8 @@ This cannot be undone! У цьому чаті заборонено безповоротне видалення повідомлень. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. У цій групі заборонено безповоротне видалення повідомлень. No comment provided by engineer. @@ -3355,6 +4390,11 @@ This cannot be undone! 3. З'єднання було скомпрометовано. 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 (%@). Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@). @@ -3373,7 +4413,7 @@ This cannot be undone! Join Приєднуйтесь - No comment provided by engineer. + swipe action Join group @@ -3415,6 +4455,11 @@ This is your link for group %@! Keep Тримай + alert action + + + Keep conversation + Підтримуйте розмову No comment provided by engineer. @@ -3425,7 +4470,7 @@ This is your link for group %@! Keep unused invitation? Зберігати невикористані запрошення? - No comment provided by engineer. + alert title Keep your connections @@ -3460,6 +4505,16 @@ This is your link for group %@! Leave Залишити + swipe action + + + Leave chat + Вийти з чату + No comment provided by engineer. + + + Leave chat? + Залишити чат? No comment provided by engineer. @@ -3502,6 +4557,18 @@ This is your link for group %@! Пов'язані робочі столи No comment provided by engineer. + + List + swipe action + + + List name and emoji should be different for all lists. + No comment provided by engineer. + + + List name... + No comment provided by engineer. + Live message! Живе повідомлення! @@ -3512,11 +4579,6 @@ This is your link for group %@! Живі повідомлення No comment provided by engineer. - - Local - Локально - No comment provided by engineer. - Local name Місцева назва @@ -3537,11 +4599,6 @@ This is your link for group %@! Режим блокування No comment provided by engineer. - - Make a private connection - Створіть приватне з'єднання - No comment provided by engineer. - Make one message disappear Зробити так, щоб одне повідомлення зникло @@ -3552,21 +4609,11 @@ This is your link for group %@! Зробіть профіль приватним! 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 Позначити видалено для всіх @@ -3592,11 +4639,35 @@ This is your link for group %@! Максимум 30 секунд, отримується миттєво. No comment provided by engineer. + + Media & file servers + Медіа та файлові сервери + No comment provided by engineer. + + + Medium + Середній + blur media + Member Учасник No comment provided by engineer. + + Member inactive + Користувач неактивний + item status text + + + Member reports + chat feature + + + 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. Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це. @@ -3607,11 +4678,64 @@ This is your link for group %@! Роль учасника буде змінено на "%@". Учасник отримає нове запрошення. 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 report messsages to moderators. + No comment provided by engineer. + + + Members can send SimpleX links. + Учасники групи можуть надсилати посилання SimpleX. + 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. + Учасники групи можуть надсилати голосові повідомлення. + No comment provided by engineer. + + + Mention members 👋 + No comment provided by engineer. + + + Menus + Меню + No comment provided by engineer. + Message delivery error Помилка доставки повідомлення @@ -3622,11 +4746,31 @@ This is your link for group %@! Підтвердження доставки повідомлення! No comment provided by engineer. + + Message delivery warning + Попередження про доставку повідомлення + item status text + Message draft Чернетка повідомлення No comment provided by engineer. + + Message forwarded + Повідомлення переслано + item status text + + + Message may be delivered later if member becomes active. + Повідомлення може бути доставлене пізніше, якщо користувач стане активним. + item status description + + + Message queue info + Інформація про чергу повідомлень + No comment provided by engineer. + Message reactions Реакції на повідомлення @@ -3637,15 +4781,41 @@ This is your link for group %@! Реакції на повідомлення в цьому чаті заборонені. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Реакції на повідомлення в цій групі заборонені. 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 + Статус повідомлення + No comment provided by engineer. + + + Message status: %@ + Статус повідомлення: %@ + copied message info + Message text Текст повідомлення @@ -3671,6 +4841,25 @@ This is your link for group %@! Повідомлення від %@ будуть показані! No comment provided by engineer. + + Messages in this chat will never be deleted. + alert message + + + Messages received + Отримані повідомлення + No comment provided by engineer. + + + Messages sent + Надіслані повідомлення + No comment provided by engineer. + + + Messages were deleted after you selected them. + Повідомлення були видалені після того, як ви їх вибрали. + alert message + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Повідомлення, файли та дзвінки захищені **наскрізним шифруванням** з ідеальною секретністю переадресації, відмовою та відновленням після злому. @@ -3736,9 +4925,9 @@ This is your link for group %@! Міграцію завершено No comment provided by engineer. - - Migrations: %@ - Міграції: %@ + + Migrations: + Міграції: No comment provided by engineer. @@ -3756,6 +4945,10 @@ This is your link for group %@! Модерується за: %@ copied message info + + More + swipe action + More improvements are coming soon! Незабаром буде ще більше покращень! @@ -3763,6 +4956,12 @@ This is your link for group %@! More reliable network connection. + Більш надійне з'єднання з мережею. + No comment provided by engineer. + + + More reliable notifications + Більш надійні сповіщення No comment provided by engineer. @@ -3770,11 +4969,6 @@ This is your link for group %@! Швидше за все, це з'єднання видалено. item status description - - Most likely this contact has deleted the connection with you. - Швидше за все, цей контакт видалив зв'язок з вами. - No comment provided by engineer. - Multiple chat profiles Кілька профілів чату @@ -3783,7 +4977,11 @@ This is your link for group %@! Mute Вимкнути звук - No comment provided by engineer. + notification label action + + + Mute all + notification label action Muted when inactive! @@ -3793,7 +4991,7 @@ This is your link for group %@! Name Ім'я - No comment provided by engineer. + swipe action Network & servers @@ -3802,10 +5000,27 @@ This is your link for group %@! Network connection + Підключення до мережі No comment provided by engineer. + + Network decentralization + Децентралізація мережі + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його. + snd error text + Network management + Керування мережею + No comment provided by engineer. + + + Network operator + Мережевий оператор No comment provided by engineer. @@ -3818,16 +5033,35 @@ This is your link for group %@! Стан мережі No comment provided by engineer. + + New + token status text + New Passcode Новий пароль 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. + No comment provided by engineer. + New chat Новий чат No comment provided by engineer. + + New chat experience 🎉 + Новий досвід спілкування в чаті 🎉 + No comment provided by engineer. + New contact request Новий запит на контакт @@ -3838,11 +5072,6 @@ This is your link for group %@! Новий контакт: notification - - New database archive - Новий архів бази даних - No comment provided by engineer. - New desktop app! Новий десктопний додаток! @@ -3853,11 +5082,21 @@ This is your link for group %@! Нове ім'я відображення No comment provided by engineer. + + New events + Нові події + notification + New in %@ Нове в %@ No comment provided by engineer. + + New media options + Нові медіа-опції + No comment provided by engineer. + New member role Нова роль учасника @@ -3873,6 +5112,11 @@ This is your link for group %@! Новий пароль… No comment provided by engineer. + + New server + Новий сервер + No comment provided by engineer. + No Ні @@ -3883,6 +5127,18 @@ This is your link for group %@! Немає пароля програми Authentication unavailable + + 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 Не вибрано жодного контакту @@ -3903,6 +5159,11 @@ This is your link for group %@! Токен пристрою відсутній! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Прямого зв'язку ще немає, повідомлення пересилається адміністратором. + item status description + No filtered chats Немає фільтрованих чатів @@ -3918,8 +5179,38 @@ This is your link for group %@! Немає історії No comment provided by engineer. + + No info, try to reload + Немає інформації, спробуйте перезавантажити + No comment provided by engineer. + + + No media & file servers. + Ніяких медіа та файлових серверів. + servers error + + + No message + No comment provided by engineer. + + + No message servers. + Ніяких серверів повідомлень. + servers error + 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. @@ -3927,16 +5218,68 @@ This is your link for group %@! Немає дозволу на запис голосового повідомлення No comment provided by engineer. + + No push server + Локально + No comment provided by engineer. + No received or sent files Немає отриманих або відправлених файлів No comment provided by engineer. + + 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. + Немає серверів для надсилання файлів. + servers error + + + No token! + alert title + + + 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 + Нічого не вибрано + No comment provided by engineer. + + + Nothing to forward! + Нічого пересилати! + alert title + Notifications Сповіщення @@ -3947,6 +5290,19 @@ This is your link for group %@! Сповіщення вимкнено! No comment provided by engineer. + + Notifications error + alert title + + + Notifications privacy + Сповіщення про приватність + No comment provided by engineer. + + + Notifications status + alert title + Now admins can: - delete members' messages. @@ -3964,36 +5320,35 @@ This is your link for group %@! Off Вимкнено - No comment provided by engineer. + blur media Ok Гаразд - No comment provided by engineer. + alert button Old database Стара база даних No comment provided by engineer. - - Old database archive - Старий архів бази даних - 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. +Requires compatible VPN. + Для підключення будуть потрібні хости onion. +Потрібно увімкнути 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. +Requires compatible VPN. + Onion хости будуть використовуватися, коли вони будуть доступні. +Потрібно увімкнути VPN. No comment provided by engineer. @@ -4001,11 +5356,21 @@ This is your link for group %@! 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. Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**. No comment provided by engineer. + + Only delete conversation + Видаляйте тільки розмови + No comment provided by engineer. + Only group owners can change group preferences. Тільки власники груп можуть змінювати налаштування групи. @@ -4021,6 +5386,14 @@ This is your link for group %@! Тільки власники груп можуть вмикати голосові повідомлення. 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. Тільки ви можете додавати реакції на повідомлення. @@ -4074,13 +5447,18 @@ This is your link for group %@! Open Відкрито - No comment provided by engineer. + alert action Open Settings Відкрийте Налаштування No comment provided by engineer. + + Open changes + Відкриті зміни + No comment provided by engineer. + Open chat Відкритий чат @@ -4091,31 +5469,45 @@ This is your link for group %@! Відкрийте консоль чату authentication reason + + Open conditions + Відкриті умови + No comment provided by engineer. + Open group Відкрита група No comment provided by engineer. + + Open link? + alert title + Open migration to another device Відкрита міграція на інший пристрій authentication reason - - Open user profiles - Відкрити профілі користувачів - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери. - No comment provided by engineer. - Opening app… Відкриваємо програму… No comment provided by engineer. + + Operator + Оператор + No comment provided by engineer. + + + Operator server + Сервер оператора + alert title + + + Or import archive file + Або імпортуйте архівний файл + No comment provided by engineer. + Or paste archive link Або вставте посилання на архів @@ -4136,10 +5528,27 @@ This is your link for group %@! Або покажіть цей код 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 file errors: +%@ + Інші помилки файлів: +%@ + alert message + PING count Кількість PING @@ -4175,6 +5584,11 @@ This is your link for group %@! Пароль встановлено! No comment provided by engineer. + + Password + Пароль + No comment provided by engineer. + Password to show Показати пароль @@ -4205,13 +5619,13 @@ This is your link for group %@! Вставте отримане посилання No comment provided by engineer. - - People can connect to you only via the links you share. - Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся. + + Pending + В очікуванні No comment provided by engineer. - - Periodically + + Periodic Періодично No comment provided by engineer. @@ -4225,11 +5639,28 @@ This is your link for group %@! Дзвінки "картинка в картинці 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. +Please share any other issues with the developers. + Переконайтеся, що мобільний і настільний комп'ютери підключені до однієї локальної мережі, і що брандмауер настільного комп'ютера дозволяє з'єднання. +Будь ласка, повідомте про будь-які інші проблеми розробникам. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше. @@ -4297,11 +5728,28 @@ Error: %@ Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите. No comment provided by engineer. + + 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. + token info + Polish interface Польський інтерфейс No comment provided by engineer. + + Port + Порт + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect Можливо, в адресі сервера неправильно вказано відбиток сертифіката @@ -4312,41 +5760,83 @@ Error: %@ Зберегти чернетку останнього повідомлення з вкладеннями. No comment provided by engineer. - - Preset server - Попередньо встановлений сервер - No comment provided by engineer. - Preset server address Попередньо встановлена адреса сервера No comment provided by engineer. + + 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 🚀 + Маршрутизація приватних повідомлень 🚀 + No comment provided by engineer. + Private notes Приватні нотатки name of notes to self + + Private routing + Приватна маршрутизація + No comment provided by engineer. + + + Private routing error + Помилка приватної маршрутизації + No comment provided by engineer. + Profile and server connections З'єднання профілю та сервера @@ -4359,16 +5849,7 @@ Error: %@ Profile images - No comment provided by engineer. - - - Profile name - Назва профілю - No comment provided by engineer. - - - Profile name: - Ім'я профілю: + Зображення профілю No comment provided by engineer. @@ -4376,10 +5857,15 @@ Error: %@ Пароль до профілю No comment provided by engineer. + + Profile theme + Тема профілю + No comment provided by engineer. + Profile update will be sent to your contacts. Оновлення профілю буде надіслано вашим контактам. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4401,8 +5887,13 @@ Error: %@ Заборонити реакції на повідомлення. No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. + Заборонити надсилання посилань SimpleX. No comment provided by engineer. @@ -4425,11 +5916,23 @@ Error: %@ Заборонити надсилання голосових повідомлень. No comment provided by engineer. + + Protect IP address + Захист IP-адреси + No comment provided by engineer. + Protect app screen Захистіть екран програми No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Захистіть свою IP-адресу від ретрансляторів повідомлень, обраних вашими контактами. +Увімкніть у налаштуваннях *Мережа та сервери*. + No comment provided by engineer. + Protect your chat profiles with a password! Захистіть свої профілі чату паролем! @@ -4445,9 +5948,24 @@ Error: %@ Тайм-аут протоколу на КБ 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 - Push-повідомлення + Push-сповіщення No comment provided by engineer. @@ -4465,6 +5983,11 @@ Error: %@ Оцініть додаток No comment provided by engineer. + + Reachable chat toolbar + Доступна панель інструментів чату + No comment provided by engineer. + React… Реагуй… @@ -4473,33 +5996,28 @@ Error: %@ Read Читати - No comment provided by engineer. + swipe action Read more Читати далі No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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). No comment provided by engineer. - - Read more in our GitHub repository. - Читайте більше в нашому репозиторії на GitHub. - 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). @@ -4510,6 +6028,11 @@ Error: %@ Підтвердження виключені No comment provided by engineer. + + Receive errors + Отримання помилок + No comment provided by engineer. + Received at Отримано за @@ -4530,15 +6053,26 @@ Error: %@ Отримано повідомлення message info title + + 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. Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі. No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. Отримання файлу буде зупинено. @@ -4556,6 +6090,7 @@ Error: %@ Recipient(s) can't see who this message is from. + Одержувач(и) не бачить, від кого це повідомлення. No comment provided by engineer. @@ -4563,11 +6098,36 @@ Error: %@ Одержувачі бачать оновлення, коли ви їх вводите. 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? Перепідключити сервери? @@ -4588,10 +6148,23 @@ Error: %@ Зменшення використання акумулятора No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject Відхилити - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4618,6 +6191,16 @@ Error: %@ Видалити No comment provided by engineer. + + Remove archive? + Видалити архів? + No comment provided by engineer. + + + Remove image + Видалити зображення + No comment provided by engineer. + Remove member Видалити учасника @@ -4678,6 +6261,46 @@ Error: %@ Відповісти chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required Потрібно @@ -4688,16 +6311,41 @@ Error: %@ Перезавантаження 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 Перезапустіть програму, щоб створити новий профіль чату @@ -4738,9 +6386,9 @@ Error: %@ Показувати chat item action - - Revert - Повернутися + + Review conditions + Умови перегляду No comment provided by engineer. @@ -4768,9 +6416,19 @@ Error: %@ Запустити чат No comment provided by engineer. - - SMP servers - Сервери SMP + + SMP server + Сервер SMP + No comment provided by engineer. + + + SOCKS proxy + Проксі SOCKS + No comment provided by engineer. + + + Safely receive files + Безпечне отримання файлів No comment provided by engineer. @@ -4781,43 +6439,43 @@ Error: %@ Save Зберегти - chat item action + alert button +chat item action Save (and notify contacts) Зберегти (і повідомити контактам) - No comment provided by engineer. + alert button Save and notify contact Зберегти та повідомити контакт - No comment provided by engineer. + alert button 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 Збережіть пароль і відкрийте чат @@ -4831,7 +6489,7 @@ Error: %@ Save preferences? Зберегти настройки? - No comment provided by engineer. + alert title Save profile password @@ -4846,20 +6504,21 @@ Error: %@ Save servers? Зберегти сервери? - No comment provided by engineer. - - - Save settings? - Зберегти налаштування? - No comment provided by engineer. + alert title Save welcome message? Зберегти вітальне повідомлення? No comment provided by engineer. + + Save your profile? + Зберегти свій профіль? + alert title + Saved + Збережено No comment provided by engineer. @@ -4869,6 +6528,7 @@ Error: %@ Saved from + Збережено з No comment provided by engineer. @@ -4876,6 +6536,21 @@ Error: %@ Збережене повідомлення message info title + + Saving %lld messages + Збереження повідомлень %lld + No comment provided by engineer. + + + Scale + Масштаб + No comment provided by engineer. + + + Scan / Paste link + Відсканувати / Вставити посилання + No comment provided by engineer. + Scan QR code Відскануйте QR-код @@ -4916,11 +6591,21 @@ Error: %@ Знайдіть або вставте посилання SimpleX No comment provided by engineer. + + Secondary + Вторинний + No comment provided by engineer. + Secure queue Безпечна черга server test step + + Secured + Забезпечено + No comment provided by engineer. + Security assessment Оцінка безпеки @@ -4934,6 +6619,21 @@ Error: %@ Select Виберіть + chat item action + + + 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. @@ -4971,11 +6671,6 @@ Error: %@ Надсилання звітів про доставку No comment provided by engineer. - - Send direct message - Надішліть пряме повідомлення - No comment provided by engineer. - Send direct message to connect Надішліть пряме повідомлення, щоб підключитися @@ -4986,6 +6681,11 @@ Error: %@ Надіслати зникаюче повідомлення No comment provided by engineer. + + Send errors + Помилки надсилання + No comment provided by engineer. + Send link previews Надіслати попередній перегляд за посиланням @@ -4996,14 +6696,28 @@ Error: %@ Надіслати живе повідомлення 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. @@ -5029,7 +6743,7 @@ Error: %@ Sender cancelled file transfer. Відправник скасував передачу файлу. - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -5086,6 +6800,11 @@ Error: %@ Надіслано за: %@ copied message info + + Sent directly + Відправлено напряму + No comment provided by engineer. + Sent file event Подія надісланого файлу @@ -5096,11 +6815,71 @@ Error: %@ Надіслано повідомлення message info title + + 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 + Сервер + No comment provided by engineer. + + + Server added to operator %@. + Сервер додано до оператора %@. + alert message + + + Server address + Адреса сервера + No comment provided by engineer. + + + Server address is incompatible with network settings. + Адреса сервера несумісна з налаштуваннями мережі. + srv error text. + + + Server address is incompatible with network settings: %@. + Адреса сервера несумісна з налаштуваннями мережі: %@. + No comment provided by engineer. + + + Server operator changed. + Оператор сервера змінився. + alert title + + + Server operators + Оператори серверів + No comment provided by engineer. + + + Server protocol changed. + Протокол сервера змінено. + alert title + Server requires authorization to create queues, check password Сервер вимагає авторизації для створення черг, перевірте пароль @@ -5116,11 +6895,36 @@ Error: %@ Тест сервера завершився невдало! No comment provided by engineer. + + Server type + Тип сервера + No comment provided by engineer. + + + Server version is incompatible with network settings. + Серверна версія несумісна з мережевими налаштуваннями. + srv error text + + + 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 Код сесії @@ -5131,11 +6935,20 @@ Error: %@ Встановити 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 Встановіть налаштування групи @@ -5146,6 +6959,10 @@ Error: %@ Встановіть його замість аутентифікації системи. No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode Встановити пароль @@ -5176,28 +6993,55 @@ Error: %@ Налаштування No comment provided by engineer. + + Settings were changed. + Налаштування були змінені. + alert message + Shape profile images + Сформуйте зображення профілю No comment provided by engineer. Share Поділіться - chat item action + alert action +chat item action Share 1-time link Поділитися 1-разовим посиланням No comment provided by engineer. + + Share 1-time link with a friend + Поділіться одноразовим посиланням з другом + No comment provided by engineer. + + + Share SimpleX address on social media. + Поділіться адресою SimpleX у соціальних мережах. + No comment provided by engineer. + Share address Поділитися адресою No comment provided by engineer. + + Share address publicly + Поділіться адресою публічно + No comment provided by engineer. + Share address with contacts? Поділіться адресою з контактами? + alert title + + + Share from other apps. + Діліться з інших програм. No comment provided by engineer. @@ -5205,16 +7049,30 @@ Error: %@ Поділіться посиланням No comment provided by engineer. + + Share profile + Поділіться профілем + 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. + + Short link + No comment provided by engineer. + Show QR code Показати QR-код @@ -5235,21 +7093,46 @@ Error: %@ Показати останні повідомлення 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 → on messages sent via private routing. + Показувати → у повідомленнях, надісланих через приватну маршрутизацію. + 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 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. @@ -5280,6 +7163,20 @@ Error: %@ Адреса 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 channel link + simplex link type + SimpleX contact address Контактна адреса SimpleX @@ -5300,12 +7197,14 @@ Error: %@ Посилання SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. + У цій групі заборонені посилання на SimpleX. No comment provided by engineer. SimpleX links not allowed + Посилання SimpleX заборонені No comment provided by engineer. @@ -5313,11 +7212,21 @@ Error: %@ Одноразове запрошення SimpleX simplex link type + + 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 Пропустити @@ -5333,18 +7242,51 @@ Error: %@ Невеликі групи (максимум 20 осіб) No comment provided by engineer. + + Soft + М'який + blur media + + + 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: + Під час імпорту виникли деякі несмертельні помилки: + No comment provided by engineer. + + + Some servers failed the test: +%@ + Деякі сервери не пройшли тестування: +%@ + alert message + Somebody Хтось notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. + Квадрат, коло або щось середнє між ними. No comment provided by engineer. @@ -5362,6 +7304,16 @@ Error: %@ Почати міграцію No comment provided by engineer. + + Starting from %@. + Починаючи з %@. + No comment provided by engineer. + + + Statistics + Статистика + No comment provided by engineer. + Stop Зупинити @@ -5377,11 +7329,6 @@ Error: %@ Припинити чат 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. Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено. @@ -5410,28 +7357,62 @@ Error: %@ Stop sharing Припиніть ділитися - No comment provided by engineer. + alert action Stop sharing address? Припинити ділитися адресою? - No comment provided by engineer. + alert title Stopping chat Зупинка чату No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + Сильний + blur media + 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 Система @@ -5442,11 +7423,20 @@ Error: %@ Автентифікація системи 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 + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5462,11 +7452,21 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + Хвіст + No comment provided by engineer. + Take picture Сфотографуйте No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше. + No comment provided by engineer. + Tap button Натисніть кнопку @@ -5502,16 +7502,20 @@ Error: %@ Натисніть, щоб сканувати No comment provided by engineer. - - Tap to start a new chat - Натисніть, щоб почати новий чат - No comment provided by engineer. + + Temporary file error + Тимчасова помилка файлу + file error alert title Test failed at step %@. Тест завершився невдало на кроці %@. server test failure + + Test notifications + No comment provided by engineer. + Test server Тестовий сервер @@ -5525,7 +7529,7 @@ Error: %@ Tests failed! Тести не пройшли! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5542,11 +7546,6 @@ Error: %@ Дякуємо користувачам - зробіть свій внесок через Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном. - No comment provided by engineer. - 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. @@ -5559,6 +7558,16 @@ It can happen because of some bug or when the connection is compromised.Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію. 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. Спроба змінити пароль до бази даних не була завершена. @@ -5569,6 +7578,11 @@ It can happen because of some bug or when the connection is compromised.Відсканований вами код не є QR-кодом посилання SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + З'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн. + No comment provided by engineer. + The connection you accepted will be cancelled! Прийняте вами з'єднання буде скасовано! @@ -5589,6 +7603,11 @@ It can happen because of some bug or when the connection is compromised.Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання! No comment provided by engineer. + + The future of messaging + Наступне покоління приватних повідомлень + No comment provided by engineer. + The hash of the previous message is different. Хеш попереднього повідомлення відрізняється. @@ -5604,9 +7623,14 @@ It can happen because of some bug or when the connection is compromised.Повідомлення буде позначено як модероване для всіх учасників. 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. @@ -5614,9 +7638,14 @@ It can happen because of some bug or when the connection is compromised.Стара база даних не була видалена під час міграції, її можна видалити. No comment provided by engineer. - - The profile is only shared with your contacts. - Профіль доступний лише вашим контактам. + + The same conditions will apply to operator **%@**. + Такі ж умови діятимуть і для оператора **%@**. + No comment provided by engineer. + + + The second preset operator in the app! + Другий попередньо встановлений оператор у застосунку! No comment provided by engineer. @@ -5634,14 +7663,29 @@ It can happen because of some bug or when the connection is compromised.Сервери для нових підключень вашого поточного профілю чату **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + Сервери для нових файлів вашого поточного профілю чату **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Текст, який ви вставили, не є посиланням SimpleX. No comment provided by engineer. - - Theme - Тема + + 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. @@ -5664,6 +7708,10 @@ It can happen because of some bug or when the connection is compromised.Цю дію неможливо скасувати - повідомлення, надіслані та отримані раніше, ніж вибрані, будуть видалені. Це може зайняти кілька хвилин. No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені. @@ -5709,11 +7757,29 @@ It can happen because of some bug or when the connection is compromised.Це ваше власне одноразове посилання! 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. + 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: Задати будь-які питання та отримувати новини: @@ -5734,9 +7800,9 @@ It can happen because of some bug or when the connection is compromised.Щоб створити нове з'єднання 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. @@ -5744,6 +7810,11 @@ It can happen because of some bug or when the connection is compromised.Для захисту часового поясу у файлах зображень/голосу використовується UTC. 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5751,6 +7822,26 @@ You will be prompted to complete authentication before this feature is enabled.< Перед увімкненням цієї функції вам буде запропоновано пройти автентифікацію. 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. Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону. @@ -5761,26 +7852,60 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**. 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. + + Token status: %@. + token status + + + 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: %@). Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@). @@ -5836,10 +7961,10 @@ You will be prompted to complete authentication before this feature is enabled.< Розблокувати учасника? No comment provided by engineer. - - Unexpected error: %@ - Неочікувана помилка: %@ - item status description + + Undelivered messages + Недоставлені повідомлення + No comment provided by engineer. Unexpected migration state @@ -5849,7 +7974,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. Нелюб. - No comment provided by engineer. + swipe action Unhide @@ -5886,6 +8011,11 @@ You will be prompted to complete authentication before this feature is enabled.< Невідома помилка No comment provided by engineer. + + Unknown servers! + Невідомі сервери! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим "Не турбувати", щоб уникнути переривань. @@ -5921,11 +8051,15 @@ To connect, please ask your contact to create another connection link and check Unmute Увімкнути звук - No comment provided by engineer. + notification label action Unread Непрочитане + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5938,11 +8072,6 @@ To connect, please ask your contact to create another connection link and check Оновлення No comment provided by engineer. - - Update .onion hosts setting? - Оновити налаштування хостів .onion? - No comment provided by engineer. - Update database passphrase Оновити парольну фразу бази даних @@ -5953,9 +8082,13 @@ To connect, please ask your contact to create another connection link and check Оновити налаштування мережі? No comment provided by engineer. - - Update transport isolation mode? - Оновити режим транспортної ізоляції? + + Update settings? + Оновити налаштування? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5963,16 +8096,16 @@ To connect, please ask your contact to create another connection link and check Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів. 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 Оновлення та відкритий чат No comment provided by engineer. + + Upload errors + Помилки завантаження + No comment provided by engineer. + Upload failed Не вдалося завантфжити @@ -5983,21 +8116,49 @@ To connect, please ask your contact to create another connection link and check Завантажити файл server test step + + Uploaded + Завантажено + No comment provided by engineer. + + + Uploaded files + Завантажені файли + No comment provided by engineer. + Uploading archive Завантаження архіву No comment provided by engineer. + + Use %@ + Використовуйте %@ + No comment provided by engineer. + Use .onion hosts Використовуйте хости .onion No comment provided by engineer. + + Use SOCKS proxy + Використовуйте SOCKS проксі + No comment provided by engineer. + Use SimpleX Chat servers? Використовувати сервери SimpleX Chat? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat Використовуйте чат @@ -6008,6 +8169,16 @@ To connect, please ask your contact to create another connection link and check Використовувати поточний профіль 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 Використовуйте для нових з'єднань @@ -6033,24 +8204,52 @@ To connect, please ask your contact to create another connection link and check Використовувати лише локальні сповіщення? 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 short links (BETA) + No comment provided by engineer. + Use the app while in the call. Використовуйте додаток під час розмови. No comment provided by engineer. - - User profile - Профіль користувача + + Use the app with one hand. + Використовуйте додаток однією рукою. No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Для використання хостів .onion потрібен сумісний VPN-провайдер. + + Use web port + No comment provided by engineer. + + + User selection + Вибір користувача + No comment provided by engineer. + + + Username + Ім'я користувача No comment provided by engineer. @@ -6123,11 +8322,21 @@ To connect, please ask your contact to create another connection link and check Відео та файли до 1 Гб No comment provided by engineer. + + View conditions + Умови перегляду + No comment provided by engineer. + View security code Переглянути код безпеки No comment provided by engineer. + + View updated conditions + Переглянути оновлені умови + No comment provided by engineer. + Visible history Видима історія @@ -6143,13 +8352,14 @@ To connect, please ask your contact to create another connection link and check Голосові повідомлення в цьому чаті заборонені. 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. @@ -6182,6 +8392,16 @@ To connect, please ask your contact to create another connection link and check Чекаємо на відео No comment provided by engineer. + + Wallpaper accent + Акцент на шпалерах + No comment provided by engineer. + + + Wallpaper background + Фон шпалер + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Попередження: запуск чату на декількох пристроях не підтримується і може призвести до збоїв у доставці повідомлень @@ -6224,11 +8444,12 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. + При підключенні аудіо та відеодзвінків. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Коли увімкнено більше одного оператора, жоден з них не має метаданих, щоб дізнатися, хто з ким спілкується. No comment provided by engineer. @@ -6238,14 +8459,17 @@ To connect, please ask your contact to create another connection link and check 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. @@ -6263,24 +8487,39 @@ To connect, please ask your contact to create another connection link and check З меншим споживанням заряду акумулятора. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Без Tor або VPN ваша IP-адреса буде видимою для файлових серверів. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів: %@. + alert message + Wrong database passphrase Неправильний пароль до бази даних No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Неправильний ключ або невідоме з'єднання - швидше за все, це з'єднання видалено. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Неправильний ключ або невідома адреса фрагмента файлу - найімовірніше, файл видалено. + file error text + Wrong passphrase! Неправильний пароль! No comment provided by engineer. - - XFTP servers - Сервери XFTP - No comment provided by engineer. - - - You - Ти + + XFTP server + XFTP-сервер No comment provided by engineer. @@ -6308,6 +8547,11 @@ To connect, please ask your contact to create another connection link and check Ви вже підключені до %@. No comment provided by engineer. + + You are already connected with %@. + Ви вже підключені до %@. + No comment provided by engineer. + You are already connecting to %@. Ви вже з'єднані з %@. @@ -6355,11 +8599,26 @@ Repeat join request? Запрошуємо вас до групи 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 can accept calls from lock screen, without device and app authentication. Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми. No comment provided by engineer. + + 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 Ви можете створити його пізніше @@ -6390,11 +8649,21 @@ Repeat join request? Ви можете зробити його видимим для ваших контактів у SimpleX за допомогою налаштувань. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Тепер ви можете надсилати повідомлення на адресу %@ notification body + + 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. Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань. @@ -6410,16 +8679,16 @@ Repeat join request? Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв'язатися з **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв'язатися з вами. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Запустити чат можна через Налаштування програми / База даних або перезапустивши програму No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Ви все ще можете переглянути розмову з %@ у списку чатів. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Увімкнути SimpleX Lock можна в Налаштуваннях. @@ -6433,23 +8702,23 @@ Repeat join request? You can view invitation link again in connection details. Ви можете переглянути посилання на запрошення ще раз у деталях підключення. - No comment provided by engineer. + alert message You can't send messages! Ви не можете надсилати повідомлення! 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 decide who can connect. + Ви вирішуєте, хто може під'єднатися. + No comment provided by engineer. + You have already requested connection via this address! Ви вже надсилали запит на підключення за цією адресою! @@ -6462,11 +8731,6 @@ Repeat connection request? Повторити запит на підключення? 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. Вам доведеться вводити парольну фразу щоразу під час запуску програми - вона не зберігається на пристрої. @@ -6487,11 +8751,26 @@ Repeat connection request? Ви приєдналися до цієї групи. Підключення до запрошеного учасника групи. 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. Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх. @@ -6507,6 +8786,10 @@ Repeat connection request? Ви надіслали запрошення до групи No comment provided by engineer. + + You should receive notifications. + token info + You will be connected to group when the group host's device is online, please wait or check later! Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше! @@ -6542,6 +8825,11 @@ Repeat connection request? Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні. 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. Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена. @@ -6562,31 +8850,16 @@ Repeat connection request? Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено No comment provided by engineer. - - Your %@ servers - Ваші сервери %@ - No comment provided by engineer. - Your ICE servers Ваші сервери ICE No comment provided by engineer. - - Your SMP servers - Ваші SMP-сервери - No comment provided by engineer. - Your SimpleX address Ваша адреса SimpleX No comment provided by engineer. - - Your XFTP servers - Ваші XFTP-сервери - No comment provided by engineer. - Your calls Твої дзвінки @@ -6602,16 +8875,19 @@ Repeat connection request? Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її. No comment provided by engineer. + + Your chat preferences + Ваші налаштування чату + alert title + Your chat profiles Ваші профілі чату No comment provided by engineer. - - 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). - Для завершення з'єднання ваш контакт має бути онлайн. -Ви можете скасувати це з'єднання і видалити контакт (і спробувати пізніше з новим посиланням). + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка. No comment provided by engineer. @@ -6629,6 +8905,11 @@ You can cancel this connection and remove the contact (and try later with a new Ваші контакти залишаться на зв'язку. No comment provided by engineer. + + Your credentials may be sent unencrypted. + Ваші облікові дані можуть бути надіслані незашифрованими. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. Ваша поточна база даних чату буде ВИДАЛЕНА і ЗАМІНЕНА імпортованою. @@ -6659,33 +8940,36 @@ You can cancel this connection and remove the contact (and try later with a new Ваш профіль **%@** буде опублікований. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. -Сервери SimpleX не бачать ваш профіль. + + Your profile is stored on your device and only shared with your contacts. + Профіль доступний лише вашим контактам. No comment provided by engineer. - - Your profile, contacts and delivered messages are stored on your device. - Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої. + + 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 was changed. If you save it, the updated profile will be sent to all your contacts. + Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам. + alert message + Your random profile Ваш випадковий профіль No comment provided by engineer. - - Your server - Ваш сервер - No comment provided by engineer. - Your server address Адреса вашого сервера No comment provided by engineer. + + Your servers + Ваші сервери + No comment provided by engineer. + Your settings Ваші налаштування @@ -6726,6 +9010,11 @@ SimpleX servers cannot see your profile. прийнято виклик call status + + accepted invitation + прийняте запрошення + chat list item title + admin адмін @@ -6733,6 +9022,7 @@ SimpleX servers cannot see your profile. admins + адміністратори feature role @@ -6747,6 +9037,7 @@ SimpleX servers cannot see your profile. all members + всі учасники feature role @@ -6759,6 +9050,15 @@ SimpleX servers cannot see your profile. та %lld інших подій No comment provided by engineer. + + archived report + No comment provided by engineer. + + + attempts + спроби + No comment provided by engineer. + audio call (not e2e encrypted) аудіовиклик (без шифрування e2e) @@ -6792,13 +9092,19 @@ SimpleX servers cannot see your profile. blocked by admin заблоковано адміністратором - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold жирний No comment provided by engineer. + + call + дзвонити + No comment provided by engineer. + call error помилка дзвінка @@ -6902,7 +9208,7 @@ SimpleX servers cannot see your profile. connecting… з'єднання… - chat list item title + No comment provided by engineer. connection established @@ -6949,10 +9255,16 @@ SimpleX servers cannot see your profile. днів time unit + + decryption errors + помилки розшифровки + No comment provided by engineer. + default (%@) за замовчуванням (%@) - pref value + delete after time +pref value default (no) @@ -6999,6 +9311,11 @@ SimpleX servers cannot see your profile. дублююче повідомлення integrity error chat item + + duplicates + дублікати + No comment provided by engineer. + e2e encrypted e2e зашифрований @@ -7074,13 +9391,14 @@ SimpleX servers cannot see your profile. помилка No comment provided by engineer. - - event happened - відбулася подія + + expired + закінчився No comment provided by engineer. forwarded + переслано No comment provided by engineer. @@ -7108,6 +9426,11 @@ SimpleX servers cannot see your profile. Пароль бази даних буде безпечно збережено в iOS Keychain після запуску чату або зміни пароля - це дасть змогу отримувати миттєві повідомлення. No comment provided by engineer. + + inactive + неактивний + No comment provided by engineer. + incognito via contact address link інкогніто за посиланням на контактну адресу @@ -7148,6 +9471,11 @@ SimpleX servers cannot see your profile. запрошення до групи %@ group name + + invite + запросити + No comment provided by engineer. + invited запрошені @@ -7203,6 +9531,11 @@ SimpleX servers cannot see your profile. з'єднаний rcv group event chat item + + message + повідомлення + No comment provided by engineer. + message received повідомлення отримано @@ -7228,6 +9561,10 @@ SimpleX servers cannot see your profile. модерується %@ marked deleted chat item preview text + + moderator + member role + months місяців @@ -7236,7 +9573,7 @@ SimpleX servers cannot see your profile. never ніколи - No comment provided by engineer. + delete after time new message @@ -7267,8 +9604,8 @@ SimpleX servers cannot see your profile. off вимкнено enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7285,6 +9622,16 @@ SimpleX servers cannot see your profile. увімкненo group pref value + + other + інший + No comment provided by engineer. + + + other errors + інші помилки + No comment provided by engineer. + owner власник @@ -7292,6 +9639,7 @@ SimpleX servers cannot see your profile. owners + власники feature role @@ -7299,6 +9647,14 @@ SimpleX servers cannot see your profile. одноранговий No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption квантово-стійке шифрування e2e @@ -7314,6 +9670,10 @@ SimpleX servers cannot see your profile. отримали підтвердження… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call відхилений виклик @@ -7344,12 +9704,24 @@ SimpleX servers cannot see your profile. прибрали вас rcv group event chat item + + requested to connect + запит на підключення + chat list item title + saved + збережено No comment provided by engineer. saved from %@ + збережено з %@ + No comment provided by engineer. + + + search + пошук No comment provided by engineer. @@ -7377,6 +9749,15 @@ SimpleX servers cannot see your profile. надіслати пряме повідомлення No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + інформація про чергу на сервері: %1$@ + +останнє отримане повідомлення: %2$@ + queue info + set new contact address встановити нову контактну адресу @@ -7417,11 +9798,21 @@ SimpleX servers cannot see your profile. невідомий connection info + + unknown servers + невідомі реле + No comment provided by engineer. + unknown status невідомий статус No comment provided by engineer. + + unprotected + незахищені + No comment provided by engineer. + updated group profile оновлений профіль групи @@ -7462,6 +9853,11 @@ SimpleX servers cannot see your profile. за допомогою ретранслятора No comment provided by engineer. + + video + відео + No comment provided by engineer. + video call (not e2e encrypted) відеодзвінок (без шифрування e2e) @@ -7487,6 +9883,11 @@ SimpleX servers cannot see your profile. тижнів time unit + + when IP hidden + коли IP приховано + No comment provided by engineer. + yes так @@ -7494,6 +9895,7 @@ SimpleX servers cannot see your profile. you + ти No comment provided by engineer. @@ -7570,7 +9972,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7607,7 +10009,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7627,4 +10029,249 @@ SimpleX servers cannot see your profile.
+ +
+ +
+ + + %d new events + %d нових подій + notification body + + + From %d chat(s) + notification body + + + From: %@ + Від: %@ + notification body + + + New events + Нові події + notification + + + New messages + Нові повідомлення + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Всі права захищені. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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. + No comment provided by engineer. + + +
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 4bf9e05665..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 @@
- +
- - - - - - 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) (可复制) @@ -109,6 +82,7 @@ %@ downloaded + %@ 已下载 No comment provided by engineer. @@ -126,13 +100,19 @@ %@ 已认证 No comment provided by engineer. + + %@ server + 服务器 + No comment provided by engineer. + %@ servers - %@ 服务器 + 服务器 No comment provided by engineer. %@ uploaded + %@ 已上传 No comment provided by engineer. @@ -140,6 +120,11 @@ %@ 要连接! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ 和 %lld 成员 @@ -160,11 +145,36 @@ %d 天 time interval + + %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 个文件。 + forward confirmation reason + %d hours %d 小时 time interval + + %d messages not forwarded + 未转发 %d 条消息 + alert title + %d min %d 分钟 @@ -180,9 +190,14 @@ %d 秒 time interval + + %d seconds(s) + %d 秒 + delete after time + %d skipped message(s) - %d 跳过消息 + 跳过的 %d 条消息 integrity error chat item @@ -227,14 +242,17 @@ %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. @@ -247,11 +265,6 @@ %lld 种新的界面语言 No comment provided by engineer. - - %lld second(s) - %lld 秒 - No comment provided by engineer. - %lld seconds %lld 秒 @@ -302,49 +315,39 @@ 已跳过 %u 条消息。 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. - - - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **添加新联系人**:为您的联系人创建一次性二维码或者链接。 + + **Create 1-time link**: to create and share a new invitation link. + **添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接. No comment provided by engineer. **Create group**: to create a new group. + **创建群组**: 创建一个新群组. 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. + + **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). + + **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**: 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. @@ -352,11 +355,16 @@ **请注意**:如果您丢失密码,您将无法恢复或者更改密码。 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. + + **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 / Paste link**: to connect via a link you received. + **扫描/粘贴链接**:用您收到的链接连接。 + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **警告**:及时推送通知需要保存在钥匙串的密码。 @@ -364,6 +372,7 @@ **Warning**: the archive will be removed. + **警告**: 存档将被删除. No comment provided by engineer. @@ -381,11 +390,6 @@ \*加粗* No comment provided by engineer. - - , - , - 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)! - delivery receipts (up to 20 members). @@ -422,13 +426,9 @@ - 编辑消息历史。 No comment provided by engineer. - - . - . - No comment provided by engineer. - 0 sec + 0 秒 time to disappear @@ -439,7 +439,8 @@ 1 day 1天 - time interval + delete after time +time interval 1 hour @@ -454,12 +455,29 @@ 1 month 1月 - time interval + delete after time +time interval 1 week 1周 - time interval + delete after time +time interval + + + 1 year + 1 年 + delete after time + + + 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 @@ -476,11 +494,6 @@ 30秒 No comment provided by engineer. - - : - : - No comment provided by engineer. - <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> @@ -530,31 +543,32 @@ 中止地址更改? No comment provided by engineer. - - About SimpleX - 关于SimpleX - No comment provided by engineer. - About SimpleX Chat 关于SimpleX Chat No comment provided by engineer. - - About SimpleX address - 关于 SimpleX 地址 + + About operators + 关于运营方 No comment provided by engineer. - - Accent color - 色调 + + Accent + 强调 No comment provided by engineer. Accept 接受 accept contact request via notification - accept incoming call via notification +accept incoming call via notification +swipe action + + + Accept conditions + 接受条款 + No comment provided by engineer. Accept connection request? @@ -569,21 +583,47 @@ Accept incognito 接受隐身聊天 - accept contact request via notification + accept contact request via notification +swipe action + + + Accepted conditions + 已接受的条款 + No comment provided by engineer. + + + Acknowledged + 确认 + No comment provided by engineer. + + + Acknowledgement errors + 确认错误 + No comment provided by engineer. + + + Active + 活跃 + token status text + + + 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 contact - 添加联系人 + + Add friends + 添加好友 No comment provided by engineer. - - Add preset servers - 添加预设服务器 + + Add list + 添加列表 No comment provided by engineer. @@ -591,14 +631,19 @@ 添加个人资料 No comment provided by engineer. + + Add server + 添加服务器 + No comment provided by engineer. + Add servers by scanning QR codes. 扫描二维码来添加服务器。 No comment provided by engineer. - - Add server… - 添加服务器… + + Add team members + 添加团队成员 No comment provided by engineer. @@ -606,11 +651,46 @@ 添加另一设备 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 地址 @@ -621,8 +701,19 @@ 将中止地址更改。将使用旧接收地址。 No comment provided by engineer. + + Address or 1-time link? + 地址还是一次性链接? + No comment provided by engineer. + + + Address settings + 地址设置 + No comment provided by engineer. + Admins can block a member for all. + 管理员可以为所有人封禁一名成员。 No comment provided by engineer. @@ -635,6 +726,16 @@ 高级网络设置 No comment provided by engineer. + + Advanced settings + 高级设置 + No comment provided by engineer. + + + All + 全部 + No comment provided by engineer. + All app data is deleted. 已删除所有应用程序数据。 @@ -645,16 +746,31 @@ 所有聊天记录和消息将被删除——这一行为无法撤销! No comment provided by engineer. + + All chats will be removed from the list %@, and the list deleted. + 列表 %@ 和其中全部聊天将被删除。 + alert message + 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. 所有群组成员将保持连接。 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! 所有消息都将被删除 - 这无法被撤销! @@ -667,6 +783,22 @@ All new messages from %@ will be hidden! + 来自 %@ 的所有新消息都将被隐藏! + No comment provided by engineer. + + + All profiles + 所有配置文件 + profile dropdown + + + All reports will be archived for you. + 将为你存档所有举报。 + No comment provided by engineer. + + + All servers + 全部服务器 No comment provided by engineer. @@ -681,6 +813,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + 你的所有联系人、对话和文件将被安全加密并分块上传到配置的 XFTP 中继。 No comment provided by engineer. @@ -693,14 +826,24 @@ 仅当您的联系人允许时才允许呼叫。 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 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. @@ -723,13 +866,24 @@ 允许发送限时消息。 No comment provided by engineer. + + 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 SimpleX links. + 允许发送 SimpleX 链接。 No comment provided by engineer. @@ -764,7 +918,7 @@ Allow your contacts to irreversibly delete sent messages. (24 hours) - 允许您的联系人不可撤回地删除已发送消息。 + 允许您的联系人不可撤回地删除已发送消息 No comment provided by engineer. @@ -792,6 +946,11 @@ 已经加入了该群组! No comment provided by engineer. + + Always use private routing. + 始终使用私有路由。 + No comment provided by engineer. + Always use relay 一直使用中继 @@ -802,11 +961,21 @@ 已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。 No comment provided by engineer. + + Another reason + 另一个理由 + report reason + Answer call 接听来电 No comment provided by engineer. + + Anybody can host servers. + 任何人都可以托管服务器。 + No comment provided by engineer. + App build: %@ 应用程序构建:%@ @@ -814,6 +983,7 @@ App data migration + 应用数据迁移 No comment provided by engineer. @@ -821,6 +991,11 @@ 应用程序为新的本地文件(视频除外)加密。 No comment provided by engineer. + + App group: + 应用组: + No comment provided by engineer. + App icon 应用程序图标 @@ -836,6 +1011,11 @@ 应用程序密码被替换为自毁密码。 No comment provided by engineer. + + App session + 应用会话 + No comment provided by engineer. + App version 应用程序版本 @@ -853,14 +1033,62 @@ 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? + 存档举报? + No comment provided by engineer. + + + Archive reports + 存档举报 + swipe action + + + Archived contacts + 已存档的联系人 No comment provided by engineer. Archiving database + 正在存档数据库 No comment provided by engineer. @@ -923,11 +1151,21 @@ 自动接受图片 No comment provided by engineer. + + Auto-accept settings + 自动接受设置 + alert title + Back 返回 No comment provided by engineer. + + Background + 背景 + No comment provided by engineer. + Bad desktop address 糟糕的桌面地址 @@ -943,16 +1181,61 @@ 错误消息散列 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 封禁 @@ -988,6 +1271,16 @@ 由管理员封禁 No comment provided by engineer. + + Blur for better privacy. + 模糊处理,提高私密性. + No comment provided by engineer. + + + Blur media + 模糊媒体 + No comment provided by engineer. + Both you and your contact can add message reactions. 您和您的联系人都可以添加消息回应。 @@ -995,7 +1288,7 @@ Both you and your contact can irreversibly delete sent messages. (24 hours) - 您和您的联系人都可以不可逆转地删除已发送的消息。 + 您和您的联系人都可以不可逆转地删除已发送的消息 No comment provided by engineer. @@ -1018,11 +1311,35 @@ 保加利亚语、芬兰语、泰语和乌克兰语——感谢用户和[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: +- send only legal content in public groups. +- respect other users – no spam. + 使用 SimpleX Chat 代表您同意: +- 在公开群中只发送合法内容 +- 尊重其他用户 – 没有垃圾信息。 + No comment provided by engineer. + Call already ended! 通话已结束! @@ -1033,11 +1350,26 @@ 通话 No comment provided by engineer. + + 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! 无法邀请联系人! @@ -1048,13 +1380,20 @@ 无法邀请联系人! No comment provided by engineer. + + Can't message member + 无法向成员发送消息 + No comment provided by engineer. + Cancel 取消 - No comment provided by engineer. + alert action +alert button Cancel migration + 取消迁移 No comment provided by engineer. @@ -1062,13 +1401,24 @@ 无法访问钥匙串以保存数据库密码 No comment provided by engineer. + + Cannot forward message + 无法转发消息 + No comment provided by engineer. + Cannot receive file 无法接收文件 - No comment provided by engineer. + alert title + + + Capacity exceeded - recipient did not receive previously sent messages. + 超出容量-收件人未收到以前发送的邮件。 + snd error text Cellular + 移动网络 No comment provided by engineer. @@ -1076,6 +1426,16 @@ 更改 No comment provided by engineer. + + Change automatic message deletion? + 更改消息自动删除设置? + alert title + + + Change chat profiles + 更改聊天资料 + authentication reason + Change database passphrase? 更改数据库密码? @@ -1120,11 +1480,26 @@ Change self-destruct passcode 更改自毁密码 authentication reason - set passcode view +set passcode view - - 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. @@ -1142,6 +1517,11 @@ 聊天数据库已删除 No comment provided by engineer. + + Chat database exported + 导出的聊天数据库 + No comment provided by engineer. + Chat database imported 聊天数据库已导入 @@ -1162,8 +1542,14 @@ 聊天已停止。如果你已经在另一台设备商使用过此数据库,你应该在启动聊天前将数据库传输回来。 No comment provided by engineer. + + Chat list + 聊天列表 + No comment provided by engineer. + Chat migrated! + 已迁移聊天! No comment provided by engineer. @@ -1171,15 +1557,50 @@ 聊天偏好设置 No comment provided by engineer. + + Chat preferences were changed. + 聊天偏好设置已修改。 + alert message + + + 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. + 在被允许时检查消息。 + No comment provided by engineer. + Check server address and try again. 检查服务器地址并再试一次。 - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1188,6 +1609,7 @@ Choose _Migrate from another device_ on the new device and scan QR code. + 在新设备上选择“从另一个设备迁移”并扫描二维码。 No comment provided by engineer. @@ -1200,10 +1622,25 @@ 从库中选择 No comment provided by engineer. + + Chunks deleted + 已删除的块 + No comment provided by engineer. + + + Chunks downloaded + 下载的块 + No comment provided by engineer. + + + Chunks uploaded + 已下载的区块 + No comment provided by engineer. + Clear 清除 - No comment provided by engineer. + swipe action Clear conversation @@ -1215,6 +1652,16 @@ 清除对话吗? No comment provided by engineer. + + Clear group? + 清除群? + No comment provided by engineer. + + + Clear or delete group? + 清除还是删除群? + No comment provided by engineer. + Clear private notes? 清除私密笔记? @@ -1225,11 +1672,21 @@ 清除验证 No comment provided by engineer. - - Colors - 颜色 + + Color chats with the new themes. + 使用新主题为聊天着色。 No comment provided by engineer. + + Color mode + 颜色模式 + No comment provided by engineer. + + + Community guidelines violation + 违反社区指导方针 + report reason + Compare file 对比文件 @@ -1240,11 +1697,56 @@ 与您的联系人比较安全码。 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 确认 @@ -1255,13 +1757,24 @@ 确认密码 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. @@ -1276,12 +1789,19 @@ Confirm that you remember database passphrase to migrate it. + 请在迁移前确认你记得数据库的密码短语。 No comment provided by engineer. Confirm upload + 确认上传 No comment provided by engineer. + + Confirmed + 已确定 + token status text + Connect 连接 @@ -1302,6 +1822,11 @@ 连接到桌面 No comment provided by engineer. + + Connect to your friends faster. + 更快地与您的朋友联系。 + No comment provided by engineer. + Connect to yourself? 连接到你自己? @@ -1310,15 +1835,20 @@ Connect to yourself? This is your own SimpleX address! + 与自己建立联系? +这是您自己的 SimpleX 地址! No comment provided by engineer. Connect to yourself? This is your own one-time link! + 与自己建立联系? +这是您自己的一次性链接! No comment provided by engineer. Connect via contact address + 通过联系地址连接 No comment provided by engineer. @@ -1333,6 +1863,12 @@ This is your own one-time link! Connect with %@ + 与 %@连接 + No comment provided by engineer. + + + Connected + 已连接 No comment provided by engineer. @@ -1340,11 +1876,21 @@ This is your own one-time link! 已连接的桌面 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 to server… 连接服务器中…… @@ -1355,6 +1901,11 @@ This is your own one-time link! 连接服务器中……(错误:%@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + 正在连接到联系人,请稍候或稍后检查! + No comment provided by engineer. + Connecting to desktop 正连接到桌面 @@ -1365,6 +1916,16 @@ This is your own one-time link! 连接 No comment provided by engineer. + + Connection and servers status. + 连接和服务器状态。 + No comment provided by engineer. + + + Connection blocked + 连接被阻止 + No comment provided by engineer. + Connection error 连接错误 @@ -1375,11 +1936,37 @@ This is your own one-time link! 连接错误(AUTH) No comment provided by engineer. + + Connection is blocked by server operator: +%@ + 连接被运营方 %@ 阻止 + 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 连接被终止 @@ -1390,6 +1977,16 @@ This is your own one-time link! 连接超时 No comment provided by engineer. + + Connection with desktop stopped + 与桌面的连接已停止 + No comment provided by engineer. + + + Connections + 连接 + No comment provided by engineer. + Contact allows 联系人允许 @@ -1400,6 +1997,11 @@ This is your own one-time link! 联系人已存在 No comment provided by engineer. + + Contact deleted! + 联系人已删除! + No comment provided by engineer. + Contact hidden: 联系人已隐藏: @@ -1410,9 +2012,9 @@ This is your own one-time link! 联系已连接 notification - - Contact is not connected yet! - 联系人尚未连接! + + Contact is deleted. + 联系人被删除。 No comment provided by engineer. @@ -1425,6 +2027,11 @@ This is your own one-time link! 联系人偏好设置 No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + 联系人将被删除-这是无法撤消的! + No comment provided by engineer. + Contacts 联系人 @@ -1435,23 +2042,44 @@ This is your own one-time link! 联系人可以将信息标记为删除;您将可以查看这些信息。 No comment provided by engineer. + + Content violates conditions of use + 内容违反使用条款 + blocking reason + Continue 继续 No comment provided by engineer. + + Conversation deleted! + 对话已删除! + No comment provided by engineer. + Copy 复制 - chat item action + 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. @@ -1459,6 +2087,11 @@ This is your own one-time link! 创建 No comment provided by engineer. + + Create 1-time link + 创建一次性链接 + No comment provided by engineer. + Create SimpleX address 创建 SimpleX 地址 @@ -1466,12 +2099,7 @@ This is your own one-time link! Create a group using a random profile. - 使用随机身份创建群组 - No comment provided by engineer. - - - Create an address to let people connect with you. - 创建一个地址,让人们与您联系。 + 使用随机身份创建群组. No comment provided by engineer. @@ -1494,6 +2122,11 @@ This is your own one-time 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/)中创建新的个人资料。 💻 @@ -1519,6 +2152,11 @@ This is your own one-time link! 创建您的资料 No comment provided by engineer. + + Created + 已创建 + No comment provided by engineer. + Created at 创建于 @@ -1526,15 +2164,12 @@ This is your own one-time link! Created at: %@ + 创建于:%@ copied message info - - Created on %@ - 创建于 %@ - No comment provided by engineer. - Creating archive link + 正在创建存档链接 No comment provided by engineer. @@ -1547,11 +2182,21 @@ This is your own one-time link! 当前密码 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 passphrase… 现有密码…… No comment provided by engineer. + + Current profile + 当前配置文件 + No comment provided by engineer. + Currently maximum supported file size is %@. 目前支持的最大文件大小为 %@。 @@ -1562,11 +2207,26 @@ This is your own one-time link! 自定义时间 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 ID 数据库 ID @@ -1665,6 +2325,11 @@ This is your own one-time link! 应用程序重新启动时将迁移数据库 No comment provided by engineer. + + Debug delivery + 调试交付 + No comment provided by engineer. + Decentralized 分散式 @@ -1678,15 +2343,17 @@ This is your own one-time link! Delete 删除 - chat item action + alert action +swipe action + + + Delete %lld messages of members? + 删除成员的 %lld 消息? + No comment provided by engineer. Delete %lld messages? - No comment provided by engineer. - - - Delete Contact - 删除联系人 + 删除 %lld 消息? No comment provided by engineer. @@ -1714,14 +2381,14 @@ This is your own one-time link! 删除并通知联系人 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. @@ -1734,6 +2401,11 @@ This is your own one-time link! 删除聊天资料? No comment provided by engineer. + + Delete chat? + 删除聊天? + No comment provided by engineer. + Delete connection 删除连接 @@ -1744,9 +2416,9 @@ This is your own one-time link! 删除联系人 No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? + 删除联系人? No comment provided by engineer. @@ -1756,6 +2428,7 @@ This cannot be undone! Delete database from this device + 从这部设备上删除数据库 No comment provided by engineer. @@ -1808,6 +2481,11 @@ This cannot be undone! 删除链接? No comment provided by engineer. + + Delete list? + 删除列表? + alert title + Delete member message? 删除成员消息? @@ -1821,7 +2499,7 @@ This cannot be undone! Delete messages 删除消息 - No comment provided by engineer. + alert button Delete messages after @@ -1838,9 +2516,9 @@ This cannot be undone! 删除旧数据库吗? No comment provided by engineer. - - Delete pending connection - 删除挂起连接 + + Delete or moderate up to 200 messages. + 允许自行删除或管理员移除最多200条消息。 No comment provided by engineer. @@ -1858,11 +2536,31 @@ This cannot be undone! 删除队列 server test step + + 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 + 删除而不通知 + No comment provided by engineer. + + + Deleted + 已删除 + No comment provided by engineer. + Deleted at 已删除于 @@ -1873,6 +2571,16 @@ This cannot be undone! 已删除于:%@ copied message info + + Deletion errors + 删除错误 + No comment provided by engineer. + + + Delivered even when Apple drops them. + 已送达,即使苹果已将其删除。 + No comment provided by engineer. + Delivery 传送 @@ -1900,6 +2608,7 @@ This cannot be undone! Desktop app version %@ is not compatible with this app. + 桌面应用程序版本 %@ 与此应用程序不兼容。 No comment provided by engineer. @@ -1907,11 +2616,41 @@ This cannot be undone! 桌面设备 No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + 目标服务器地址 %@ 与转发服务器 %@ 设置不兼容。 + No comment provided by engineer. + + + Destination server error: %@ + 目标服务器错误:%@ + snd error text + + + 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 开发者工具 @@ -1924,7 +2663,7 @@ This cannot be undone! Device authentication is disabled. Turning off SimpleX Lock. - 设备验证被禁用。关闭 SimpleX 锁定。 + 设备验证已禁用。 SimpleX 已解锁。 No comment provided by engineer. @@ -1942,9 +2681,14 @@ This cannot be undone! 私信 chat feature - - 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. @@ -1957,11 +2701,26 @@ This cannot be undone! 禁用 SimpleX 锁定 authentication reason + + Disable automatic message deletion? + 禁用消息自动销毁? + alert title + + + Disable delete messages + 停用消息删除 + alert button + Disable for all 全部禁用 No comment provided by engineer. + + Disabled + 禁用 + No comment provided by engineer. + Disappearing message 限时消息 @@ -1977,8 +2736,8 @@ This cannot be undone! 此聊天中禁止显示限时消息。 No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. 该组禁止限时消息。 No comment provided by engineer. @@ -2012,11 +2771,21 @@ This cannot be undone! 通过本地网络发现 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 SimpleX for emergency calls. 请勿使用 SimpleX 进行紧急通话。 No comment provided by engineer. + + Do NOT use private routing. + 不要使用私有路由。 + No comment provided by engineer. + Do it later 稍后再做 @@ -2027,6 +2796,16 @@ This cannot be undone! 不给新成员发送历史消息。 No comment provided by engineer. + + Do not use credentials with proxy. + 代理不使用身份验证凭据。 + No comment provided by engineer. + + + Documents: + 文档: + No comment provided by engineer. + Don't create address 不创建地址 @@ -2037,11 +2816,21 @@ This cannot be undone! 不要启用 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 降级并打开聊天 @@ -2049,10 +2838,18 @@ This cannot be undone! Download - chat item action + 下载 + alert button +chat item action + + + Download errors + 下载错误 + No comment provided by engineer. Download failed + 下载失败了 No comment provided by engineer. @@ -2060,12 +2857,29 @@ This cannot be undone! 下载文件 server test step + + Download files + 下载文件 + alert action + + + 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. @@ -2078,6 +2892,11 @@ This cannot be undone! 时长 No comment provided by engineer. + + E2E encrypted notifications. + 端到端加密的通知。 + No comment provided by engineer. + Edit 编辑 @@ -2098,6 +2917,11 @@ This cannot be undone! 启用(保持覆盖) No comment provided by engineer. + + Enable Flux in Network & servers settings for better metadata privacy. + 在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。 + No comment provided by engineer. + Enable SimpleX Lock 启用 SimpleX 锁定 @@ -2111,7 +2935,7 @@ This cannot be undone! Enable automatic message deletion? 启用自动删除消息? - No comment provided by engineer. + alert title Enable camera access @@ -2125,6 +2949,7 @@ This cannot be undone! Enable in direct chats (BETA)! + 在私聊中开启(公测)! No comment provided by engineer. @@ -2157,8 +2982,14 @@ This cannot be undone! 启用自毁密码 set passcode view + + Enabled + 已启用 + No comment provided by engineer. + Enabled for + 启用对象 No comment provided by engineer. @@ -2193,6 +3024,7 @@ This cannot be undone! Encrypted message: app is stopped + 加密消息:应用程序已停止 notification @@ -2230,6 +3062,11 @@ This cannot be undone! 加密重协商失败了。 No comment provided by engineer. + + Encryption renegotiation in progress. + 正进行加密重协商。 + No comment provided by engineer. + Enter Passcode 输入密码 @@ -2242,10 +3079,12 @@ This cannot be undone! Enter group name… + 输入组名称… No comment provided by engineer. Enter passphrase + 输入密码短语 No comment provided by engineer. @@ -2280,6 +3119,7 @@ This cannot be undone! Enter your name… + 请输入您的姓名… No comment provided by engineer. @@ -2292,26 +3132,36 @@ This cannot be undone! 中止地址更改错误 No comment provided by engineer. + + Error accepting conditions + 接受条款出错 + alert title + Error accepting contact request 接受联系人请求错误 No comment provided by engineer. - - Error accessing database file - 访问数据库文件错误 - No comment provided by engineer. - Error adding member(s) 添加成员错误 No comment provided by engineer. + + Error adding server + 添加服务器出错 + alert title + Error changing address 更改地址错误 No comment provided by engineer. + + Error changing connection profile + 更改连接资料出错 + No comment provided by engineer. + Error changing role 更改角色错误 @@ -2322,6 +3172,20 @@ This cannot be undone! 更改设置错误 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 创建地址错误 @@ -2337,6 +3201,11 @@ This cannot be undone! 创建群组链接错误 No comment provided by engineer. + + Error creating list + 创建列表出错 + alert title + Error creating member contact 创建成员联系人时出错 @@ -2352,6 +3221,11 @@ This cannot be undone! 创建资料错误! No comment provided by engineer. + + Error creating report + 创建举报出错 + No comment provided by engineer. + Error decrypting file 解密文件时出错 @@ -2372,11 +3246,6 @@ This cannot be undone! 删除连接错误 No comment provided by engineer. - - Error deleting contact - 删除联系人错误 - No comment provided by engineer. - Error deleting database 删除数据库错误 @@ -2399,6 +3268,7 @@ This cannot be undone! Error downloading the archive + 下载存档出错 No comment provided by engineer. @@ -2421,6 +3291,11 @@ This cannot be undone! 导出聊天数据库错误 No comment provided by engineer. + + Error exporting theme: %@ + 导出主题时出错: %@ + No comment provided by engineer. + Error importing chat database 导入聊天数据库错误 @@ -2431,28 +3306,54 @@ This cannot be undone! 加入群组错误 No comment provided by engineer. - - Error loading %@ servers - 加载 %@ 服务器错误 + + Error loading servers + 加载服务器出错 + alert title + + + Error migrating settings + 迁移设置出错 No comment provided by engineer. Error opening chat + 打开聊天时出错 No comment provided by engineer. Error receiving file 接收文件错误 + alert title + + + Error reconnecting server + 重新连接服务器时出错 No comment provided by engineer. + + Error reconnecting servers + 重新连接服务器时出错 + No comment provided by engineer. + + + Error registering for notifications + 注册消息推送出错 + alert title + Error removing member 删除成员错误 No comment provided by engineer. - - Error saving %@ servers - 保存 %@ 服务器错误 + + Error reordering lists + 重排列表出错 + alert title + + + Error resetting statistics + 重置统计信息时出错 No comment provided by engineer. @@ -2460,6 +3361,11 @@ This cannot be undone! 保存 ICE 服务器错误 No comment provided by engineer. + + Error saving chat list + 保存聊天列表出错 + alert title + Error saving group profile 保存群组资料错误 @@ -2475,8 +3381,14 @@ This cannot be undone! 保存密码到钥匙串错误 No comment provided by engineer. + + Error saving servers + 保存服务器出错 + alert title + Error saving settings + 保存设置出错 when migrating @@ -2486,6 +3398,7 @@ This cannot be undone! Error scanning code: %@ + 扫描代码时出错:%@ No comment provided by engineer. @@ -2518,16 +3431,26 @@ This cannot be undone! 停止聊天错误 No comment provided by engineer. + + Error switching profile + 切换配置文件出错 + No comment provided by engineer. + Error switching profile! 切换资料错误! - No comment provided by engineer. + alertTitle Error synchronizing connection 同步连接错误 No comment provided by engineer. + + Error testing server connection + 检验服务器连接出错 + No comment provided by engineer. + Error updating group link 更新群组链接错误 @@ -2538,6 +3461,11 @@ This cannot be undone! 更新消息错误 No comment provided by engineer. + + Error updating server + 更新服务器出错 + alert title + Error updating settings 更新设置错误 @@ -2550,10 +3478,12 @@ This cannot be undone! Error uploading the archive + 上传存档出错 No comment provided by engineer. Error verifying passphrase: + 验证密码短语出错: No comment provided by engineer. @@ -2564,7 +3494,9 @@ This cannot be undone! Error: %@ 错误: %@ - No comment provided by engineer. + alert message +file error text +snd error text Error: URL is invalid @@ -2576,6 +3508,16 @@ This cannot be undone! 错误:没有数据库文件 No comment provided by engineer. + + Errors + 错误 + No comment provided by engineer. + + + Errors in servers configuration. + 服务器配置有错误。 + servers error + Even when disabled in the conversation. 即使在对话中被禁用。 @@ -2591,6 +3533,11 @@ This cannot be undone! 展开 chat item action + + Expired + 已过期 + token status text + Export database 导出数据库 @@ -2601,6 +3548,11 @@ This cannot be undone! 导出错误: No comment provided by engineer. + + Export theme + 导出主题 + No comment provided by engineer. + Exported database archive. 导出数据库归档。 @@ -2608,6 +3560,7 @@ This cannot be undone! Exported file doesn't exist + 导出的文件不存在 No comment provided by engineer. @@ -2625,16 +3578,70 @@ This cannot be undone! 快速且无需等待发件人在线! 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. + 更快发送消息。 + No comment provided by engineer. + Favorite 最喜欢 + swipe action + + + Favorites + 收藏 No comment provided by engineer. + + File error + 文件错误 + file error alert title + + + File errors: +%@ + 文件错误: +%@ + alert message + + + File is blocked by server operator: +%@. + 文件被服务器运营方阻止: +%@。 + file error text + + + File not found - most likely file was deleted or cancelled. + 找不到文件 - 很可能文件已被删除或取消。 + file error text + + + File server error: %@ + 文件服务器错误:%@ + file error text + + + File status + 文件状态 + No comment provided by engineer. + + + File status: %@ + 文件状态:%@ + copied message info + File will be deleted from servers. 文件将从服务器中删除。 @@ -2655,6 +3662,11 @@ This cannot be undone! 文件:%@ No comment provided by engineer. + + Files + 文件 + No comment provided by engineer. + Files & media 文件和媒体 @@ -2665,13 +3677,14 @@ This cannot be undone! 文件和媒体 chat feature - - 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. @@ -2686,10 +3699,12 @@ This cannot be undone! Finalize migration + 完成迁移 No comment provided by engineer. Finalize migration on another device. + 在另一部设备上完成迁移. No comment provided by engineer. @@ -2732,27 +3747,115 @@ This cannot be undone! 修复群组成员不支持的问题 No comment provided by engineer. + + For all moderators + 所有 moderators + No comment provided by engineer. + + + For chat profile %@: + 为聊天资料 %@: + servers error + 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 + 用于社交媒体 + No comment provided by engineer. + Forward + 转发 chat item action + + Forward %d message(s)? + 转发 %d 条消息? + alert title + Forward and save messages + 转发并保存消息 + No comment provided by engineer. + + + Forward messages + 已转发的消息 + alert action + + + Forward messages without files? + 仅转发消息不转发文件? + alert message + + + Forward up to 20 messages at once. + 一次转发最多20条消息。 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: %@. + 转发服务器版本与网络设置不兼容:%@。 + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + 转发服务器: %1$@ +目标服务器错误: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + 转发服务器: %1$@ +错误: %2$@ + snd error text + Found desktop 找到了桌面 @@ -2773,11 +3876,6 @@ This cannot be undone! 全名(可选) No comment provided by engineer. - - Full name: - 全名: - No comment provided by engineer. - Fully decentralized – visible only to members. 完全去中心化 - 仅对成员可见。 @@ -2798,6 +3896,21 @@ This cannot be undone! GIF 和贴纸 No comment provided by engineer. + + Get notified when mentioned. + 被提及时收到通知。 + No comment provided by engineer. + + + Good afternoon! + 下午好! + message preview + + + Good morning! + 早上好! + message preview + Group 群组 @@ -2805,6 +3918,7 @@ This cannot be undone! Group already exists + 群组已存在 No comment provided by engineer. @@ -2852,40 +3966,6 @@ This cannot be undone! 群组链接 No comment provided by engineer. - - Group members can add message reactions. - 群组成员可以添加信息回应。 - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. (24 hours) - 群组成员可以不可撤回地删除已发送的消息。 - No comment provided by engineer. - - - Group members can send SimpleX links. - 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. - 群组成员可以发送语音消息。 - No comment provided by engineer. - Group message: 群组消息: @@ -2926,11 +4006,21 @@ 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 隐藏 @@ -2981,10 +4071,20 @@ This cannot be undone! SimpleX的工作原理 No comment provided by engineer. + + How it affects privacy + 它如何影响隐私 + No comment provided by engineer. + + + How it helps privacy + 它如何帮助隐私 + No comment provided by engineer. + How it works 工作原理 - No comment provided by engineer. + alert button How to @@ -3003,6 +4103,7 @@ This cannot be undone! Hungarian interface + 匈牙利语界面 No comment provided by engineer. @@ -3010,6 +4111,11 @@ This cannot be undone! ICE 服务器(每行一个) No comment provided by engineer. + + IP address + IP 地址 + No comment provided by engineer. + If you can't meet in person, show QR code in a video call, or share the link. 如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。 @@ -3050,8 +4156,8 @@ This cannot be undone! 立即 No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam 不受垃圾和骚扰消息影响 No comment provided by engineer. @@ -3072,10 +4178,24 @@ This cannot be undone! 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. +More improvements are coming soon! + 改善传送,降低流量使用。 +更多改进即将推出! No comment provided by engineer. @@ -3095,6 +4215,7 @@ This cannot be undone! In order to continue, chat should be stopped. + 必须停止聊天才能继续。 No comment provided by engineer. @@ -3104,8 +4225,19 @@ This cannot be undone! In-call sounds + 通话声音 No comment provided by engineer. + + Inappropriate content + 不当内容 + report reason + + + Inappropriate profile + 不当个人资料 + report reason + Incognito 隐身聊天 @@ -3176,6 +4308,11 @@ This cannot be undone! 安装[用于终端的 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! @@ -3183,16 +4320,41 @@ This cannot be undone! No comment provided by engineer. - - Instantly - 即时 - No comment provided by engineer. - Interface 界面 No comment provided by engineer. + + Interface colors + 界面颜色 + No comment provided by engineer. + + + Invalid + 无效 + token status text + + + Invalid (bad token) + Token 无效 + token status text + + + Invalid (expired) + 无效(已过期) + token status text + + + Invalid (unregistered) + 无效(未注册) + token status text + + + Invalid (wrong topic) + 无效(话题有误) + token status text + Invalid QR code 无效的二维码 @@ -3210,10 +4372,12 @@ This cannot be undone! Invalid link + 无效链接 No comment provided by engineer. Invalid migration confirmation + 迁移确认无效 No comment provided by engineer. @@ -3223,12 +4387,13 @@ This cannot be undone! Invalid response + 无效的响应 No comment provided by engineer. Invalid server address! 无效的服务器地址! - No comment provided by engineer. + alert title Invalid status @@ -3250,6 +4415,11 @@ This cannot be undone! 邀请成员 No comment provided by engineer. + + Invite to chat + 邀请加入聊天 + No comment provided by engineer. + Invite to group 邀请加入群组 @@ -3265,8 +4435,8 @@ This cannot be undone! 此聊天中禁止不可撤回消息移除。 No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. 此群组中禁止不可撤回消息移除。 No comment provided by engineer. @@ -3291,6 +4461,11 @@ This cannot be undone! 3.连接被破坏。 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 (%@). 您似乎已经通过此链接连接。如果不是这样,则有一个错误 (%@)。 @@ -3309,7 +4484,7 @@ This cannot be undone! Join 加入 - No comment provided by engineer. + swipe action Join group @@ -3333,11 +4508,14 @@ This cannot be undone! Join with current profile + 使用当前档案加入 No comment provided by engineer. Join your group? This is your link for group %@! + 加入您的群组? +这是您组 %@ 的链接! No comment provided by engineer. @@ -3348,16 +4526,22 @@ This is your link for group %@! Keep 保留 + alert action + + + Keep conversation + 保持对话 No comment provided by engineer. Keep the app open to use it from desktop + 保持应用程序打开状态以从桌面使用它 No comment provided by engineer. Keep unused invitation? 保留未使用的邀请吗? - No comment provided by engineer. + alert title Keep your connections @@ -3392,6 +4576,16 @@ This is your link for group %@! Leave 离开 + swipe action + + + Leave chat + 离开聊天 + No comment provided by engineer. + + + Leave chat? + 离开聊天? No comment provided by engineer. @@ -3434,6 +4628,21 @@ This is your link for group %@! 已链接桌面 No comment provided by engineer. + + List + 列表 + swipe action + + + List name and emoji should be different for all lists. + 所有列表的名称和表情符号都应不同。 + No comment provided by engineer. + + + List name... + 列表名… + No comment provided by engineer. + Live message! 实时消息! @@ -3444,11 +4653,6 @@ This is your link for group %@! 实时消息 No comment provided by engineer. - - Local - 本地 - No comment provided by engineer. - Local name 本地名称 @@ -3469,11 +4673,6 @@ This is your link for group %@! 锁定模式 No comment provided by engineer. - - Make a private connection - 建立私密连接 - No comment provided by engineer. - Make one message disappear 使一条消息消失 @@ -3484,21 +4683,11 @@ This is your link for group %@! 将个人资料设为私密! 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 标记为所有人已删除 @@ -3524,11 +4713,36 @@ This is your link for group %@! 最长30秒,立即接收。 No comment provided by engineer. + + Media & file servers + Media & file servers + No comment provided by engineer. + + + Medium + 中等 + blur media + Member 成员 No comment provided by engineer. + + Member inactive + 成员不活跃 + item status text + + + Member reports + 成员举报 + chat feature + + + 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. 成员角色将更改为 "%@"。所有群成员将收到通知。 @@ -3539,11 +4753,66 @@ This is your link for group %@! 成员角色将更改为 "%@"。该成员将收到一份新的邀请。 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 SimpleX links. + 群成员可发送 SimpleX 链接。 + 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. + 群组成员可以发送语音消息。 + No comment provided by engineer. + + + Mention members 👋 + 提及成员👋 + No comment provided by engineer. + + + Menus + 菜单 + No comment provided by engineer. + Message delivery error 消息传递错误 @@ -3554,11 +4823,31 @@ This is your link for group %@! 消息送达回执! No comment provided by engineer. + + Message delivery warning + 消息传递警告 + item status text + Message draft 消息草稿 No comment provided by engineer. + + Message forwarded + 消息已转发 + item status text + + + Message may be delivered later if member becomes active. + 如果 member 变为活动状态,则稍后可能会发送消息。 + item status description + + + Message queue info + 消息队列信息 + No comment provided by engineer. + Message reactions 消息回应 @@ -3569,15 +4858,41 @@ This is your link for group %@! 该聊天禁用了消息回应。 No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. 该群组禁用了消息回应。 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 + 消息状态 + No comment provided by engineer. + + + Message status: %@ + 消息状态:%@ + copied message info + Message text 消息正文 @@ -3585,6 +4900,7 @@ This is your link for group %@! Message too large + 消息太大了 No comment provided by engineer. @@ -3599,38 +4915,67 @@ This is your link for group %@! Messages from %@ will be shown! + 将显示来自 %@ 的消息! No comment provided by engineer. + + Messages in this chat will never be deleted. + 此聊天中的消息永远不会被删除。 + alert message + + + Messages received + 收到的消息 + No comment provided by engineer. + + + Messages sent + 已发送的消息 + No comment provided by engineer. + + + Messages were deleted after you selected them. + 在你选中消息后这些消息已被删除。 + alert message + 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. @@ -3640,6 +4985,7 @@ This is your link for group %@! Migration complete + 迁移完毕 No comment provided by engineer. @@ -3657,9 +5003,9 @@ This is your link for group %@! 迁移完成 No comment provided by engineer. - - Migrations: %@ - 迁移:%@ + + Migrations: + 迁移 No comment provided by engineer. @@ -3677,6 +5023,11 @@ This is your link for group %@! 已被管理员移除于:%@ copied message info + + More + 更多 + swipe action + More improvements are coming soon! 更多改进即将推出! @@ -3684,6 +5035,12 @@ This is your link for group %@! More reliable network connection. + 更可靠的网络连接。 + No comment provided by engineer. + + + More reliable notifications + 更可靠的通知 No comment provided by engineer. @@ -3691,11 +5048,6 @@ This is your link for group %@! 此连接很可能已被删除。 item status description - - Most likely this contact has deleted the connection with you. - 很可能此联系人已经删除了与您的联系。 - No comment provided by engineer. - Multiple chat profiles 多个聊天资料 @@ -3704,7 +5056,12 @@ This is your link for group %@! Mute 静音 - No comment provided by engineer. + notification label action + + + Mute all + 全部静音 + notification label action Muted when inactive! @@ -3714,7 +5071,7 @@ This is your link for group %@! Name 名称 - No comment provided by engineer. + swipe action Network & servers @@ -3723,10 +5080,27 @@ This is your link for group %@! Network connection + 网络连接 No comment provided by engineer. + + Network decentralization + 网络去中心化 + No comment provided by engineer. + + + Network issues - message expired after many attempts to send it. + 网络问题 - 消息在多次尝试发送后过期。 + snd error text + Network management + 网络管理 + No comment provided by engineer. + + + Network operator + 网络运营方 No comment provided by engineer. @@ -3739,16 +5113,36 @@ This is your link for group %@! 网络状态 No comment provided by engineer. + + New + + token status text + New Passcode 新密码 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 凭据。 + No comment provided by engineer. + New chat 新聊天 No comment provided by engineer. + + New chat experience 🎉 + 新的聊天体验 🎉 + No comment provided by engineer. + New contact request 新联系人请求 @@ -3759,11 +5153,6 @@ This is your link for group %@! 新联系人: notification - - New database archive - 新数据库存档 - No comment provided by engineer. - New desktop app! 全新桌面应用! @@ -3774,11 +5163,21 @@ This is your link for group %@! 新显示名 No comment provided by engineer. + + New events + 新事件 + notification + New in %@ %@ 的新内容 No comment provided by engineer. + + New media options + 新媒体选项 + No comment provided by engineer. + New member role 新成员角色 @@ -3794,6 +5193,11 @@ This is your link for group %@! 新密码…… No comment provided by engineer. + + New server + 新服务器 + No comment provided by engineer. + No @@ -3804,6 +5208,21 @@ This is your link for group %@! 没有应用程序密码 Authentication unavailable + + 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 未选择联系人 @@ -3824,6 +5243,11 @@ This is your link for group %@! 无设备令牌! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + 还没有直接连接,消息由管理员转发。 + item status description + No filtered chats 无过滤聊天 @@ -3839,8 +5263,39 @@ This is your link for group %@! 无历史记录 No comment provided by engineer. + + No info, try to reload + 无信息,尝试重新加载 + No comment provided by engineer. + + + No media & file servers. + 无媒体和文件服务器。 + servers error + + + No message + 无消息 + No comment provided by engineer. + + + No message servers. + 无消息服务器。 + servers error + 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. @@ -3848,16 +5303,71 @@ This is your link for group %@! 没有录制语音消息的权限 No comment provided by engineer. + + No push server + 本地 + No comment provided by engineer. + No received or sent files 未收到或发送文件 No comment provided by engineer. + + 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. + 无文件发送服务器。 + servers error + + + No token! + 无 token! + alert title + + + 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 + 未选中任何内容 + No comment provided by engineer. + + + Nothing to forward! + 无可转发! + alert title + Notifications 通知 @@ -3868,6 +5378,21 @@ This is your link for group %@! 通知被禁用! No comment provided by engineer. + + Notifications error + 通知错误 + alert title + + + Notifications privacy + 通知隐私 + No comment provided by engineer. + + + Notifications status + 通知状态 + alert title + Now admins can: - delete members' messages. @@ -3885,36 +5410,35 @@ This is your link for group %@! Off 关闭 - No comment provided by engineer. + blur media Ok 好的 - No comment provided by engineer. + alert button Old database 旧的数据库 No comment provided by engineer. - - Old database archive - 旧数据库存档 - 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. +Requires compatible VPN. + Onion 主机将是连接所必需的。 +需要兼容的 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. +Requires compatible VPN. + 如果可用,将使用洋葱主机。 +需要兼容的 VPN。 No comment provided by engineer. @@ -3922,11 +5446,21 @@ This is your link for group %@! 将不会使用 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. 只有群主可以改变群组偏好设置。 @@ -3942,6 +5476,16 @@ This is your link for group %@! 只有群主可以启用语音信息。 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. 只有您可以添加消息回应。 @@ -3949,7 +5493,7 @@ This is your link for group %@! Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours) - 只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。 + 只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除) No comment provided by engineer. @@ -3974,7 +5518,7 @@ This is your link for group %@! Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours) - 只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。 + 只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除) No comment provided by engineer. @@ -3995,13 +5539,18 @@ This is your link for group %@! Open 打开 - No comment provided by engineer. + alert action Open Settings 打开设置 No comment provided by engineer. + + Open changes + 打开更改 + No comment provided by engineer. + Open chat 打开聊天 @@ -4012,31 +5561,48 @@ This is your link for group %@! 打开聊天控制台 authentication reason + + Open conditions + 打开条款 + No comment provided by engineer. + Open group 打开群 No comment provided by engineer. + + Open link? + alert title + Open migration to another device + 打开迁移到另一台设备 authentication reason - - Open user profiles - 打开用户个人资料 - authentication reason - - - Open-source protocol and code – anybody can run the servers. - 开源协议和代码——任何人都可以运行服务器。 - No comment provided by engineer. - Opening app… + 正在打开应用程序… + No comment provided by engineer. + + + Operator + 运营方 + No comment provided by engineer. + + + Operator server + 运营方服务器 + alert title + + + Or import archive file + 或者导入或者导入压缩文件 No comment provided by engineer. Or paste archive link + 或粘贴存档链接 No comment provided by engineer. @@ -4046,6 +5612,7 @@ This is your link for group %@! Or securely share this file link + 或安全地分享此文件链接 No comment provided by engineer. @@ -4053,10 +5620,26 @@ This is your link for group %@! 或者显示此码 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 file errors: +%@ + alert message + PING count PING 次数 @@ -4092,6 +5675,11 @@ This is your link for group %@! 密码已设置! No comment provided by engineer. + + Password + 密码 + No comment provided by engineer. + Password to show 显示密码 @@ -4099,6 +5687,7 @@ This is your link for group %@! Past member %@ + 前任成员 %@ past/unknown group member @@ -4121,13 +5710,13 @@ This is your link for group %@! 粘贴您收到的链接 No comment provided by engineer. - - People can connect to you only via the links you share. - 人们只能通过您共享的链接与您建立联系。 + + Pending + 待定 No comment provided by engineer. - - Periodically + + Periodic 定期 No comment provided by engineer. @@ -4138,6 +5727,17 @@ This is your link for group %@! Picture-in-picture calls + 画中画通话 + 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. @@ -4145,6 +5745,13 @@ This is your link for group %@! 请让您的联系人启用发送语音消息。 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. +Please share any other issues with the developers. + 请检查移动设备和桌面是否连接到同一本地网络,以及桌面防火墙是否允许连接。 +请与开发人员分享任何其他问题。 + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. 请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。 @@ -4162,11 +5769,14 @@ This is your link for group %@! Please confirm that network settings are correct for this device. + 请确认网络设置对此这台设备正确无误。 No comment provided by engineer. Please contact developers. Error: %@ + 请联系开发人员。 +错误:%@ No comment provided by engineer. @@ -4209,11 +5819,27 @@ Error: %@ 请安全地保存密码,如果您丢失了密码,您将无法更改它。 No comment provided by engineer. + + 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. + token info + Polish interface 波兰语界面 No comment provided by engineer. + + Port + No comment provided by engineer. + Possibly, certificate fingerprint in server address is incorrect 服务器地址中的证书指纹可能不正确 @@ -4224,41 +5850,83 @@ Error: %@ 保留最后的消息草稿及其附件。 No comment provided by engineer. - - Preset server - 预设服务器 - No comment provided by engineer. - Preset server address 预设服务器地址 No comment provided by engineer. + + 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 🚀 + 私有消息路由 🚀 + No comment provided by engineer. + Private notes 私密笔记 name of notes to self + + Private routing + 专用路由 + No comment provided by engineer. + + + Private routing error + 专用路由错误 + No comment provided by engineer. + Profile and server connections 资料和服务器连接 @@ -4271,15 +5939,7 @@ Error: %@ Profile images - No comment provided by engineer. - - - Profile name - No comment provided by engineer. - - - Profile name: - 显示名: + 个人资料图 No comment provided by engineer. @@ -4287,10 +5947,15 @@ Error: %@ 个人资料密码 No comment provided by engineer. + + Profile theme + 个人资料主题 + No comment provided by engineer. + Profile update will be sent to your contacts. 个人资料更新将被发送给您的联系人。 - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4312,8 +5977,13 @@ Error: %@ 禁止消息回应。 No comment provided by engineer. + + Prohibit reporting messages to moderators. + No comment provided by engineer. + Prohibit sending SimpleX links. + 禁止发送 SimpleX 链接。 No comment provided by engineer. @@ -4336,11 +6006,23 @@ Error: %@ 禁止发送语音消息。 No comment provided by engineer. + + Protect IP address + 保护 IP 地址 + No comment provided by engineer. + Protect app screen 保护应用程序屏幕 No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + 保护您的 IP 地址免受联系人选择的消息中继的攻击。 +在*网络和服务器*设置中启用。 + No comment provided by engineer. + Protect your chat profiles with a password! 使用密码保护您的聊天资料! @@ -4356,6 +6038,20 @@ Error: %@ 每 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 推送通知 @@ -4363,10 +6059,12 @@ Error: %@ Push server + 推送服务器 No comment provided by engineer. Quantum resistant encryption + 抗量子加密 No comment provided by engineer. @@ -4374,6 +6072,11 @@ Error: %@ 评价此应用程序 No comment provided by engineer. + + Reachable chat toolbar + 可访问的聊天工具栏 + No comment provided by engineer. + React… 回应… @@ -4382,33 +6085,28 @@ Error: %@ Read 已读 - No comment provided by engineer. + swipe action Read more 阅读更多 No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - 在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。 - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/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) 中阅读更多内容。 No comment provided by engineer. - - Read more in our GitHub repository. - 在我们的 GitHub 仓库中阅读更多内容。 - 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) 中阅读更多信息。 @@ -4419,6 +6117,11 @@ Error: %@ 回执已禁用 No comment provided by engineer. + + Receive errors + 接收错误 + No comment provided by engineer. + Received at 已收到于 @@ -4439,15 +6142,26 @@ Error: %@ 收到的信息 message info title + + 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. 接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。 No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. 即将停止接收文件。 @@ -4460,10 +6174,12 @@ Error: %@ 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. @@ -4471,11 +6187,36 @@ Error: %@ 对方会在您键入时看到更新。 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? 是否重新连接服务器? @@ -4496,10 +6237,23 @@ Error: %@ 减少电池使用量 No comment provided by engineer. + + Register + No comment provided by engineer. + + + Register notification token? + token info + + + Registered + token status text + Reject 拒绝 - reject incoming call via notification + reject incoming call via notification +swipe action Reject (sender NOT notified) @@ -4526,6 +6280,15 @@ Error: %@ 移除 No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + + + Remove image + 移除图片 + No comment provided by engineer. + Remove member 删除成员 @@ -4563,10 +6326,12 @@ Error: %@ Repeat download + 重复下载 No comment provided by engineer. Repeat import + 重复导入 No comment provided by engineer. @@ -4576,6 +6341,7 @@ Error: %@ Repeat upload + 重复上传 No comment provided by engineer. @@ -4583,6 +6349,46 @@ Error: %@ 回复 chat item action + + Report + chat item action + + + 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. + report reason + + + Report reason? + No comment provided by engineer. + + + Report spam: only group moderators will see it. + report reason + + + Report violation: only group moderators will see it. + report reason + + + Report: %@ + report in notification + + + Reporting messages to moderators is prohibited. + No comment provided by engineer. + + + Reports + No comment provided by engineer. + Required 必须 @@ -4593,16 +6399,41 @@ Error: %@ 重置 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 重新启动应用程序以创建新的聊天资料 @@ -4643,24 +6474,24 @@ Error: %@ 揭示 chat item action - - Revert - 恢复 + + Review conditions + 审阅条款 No comment provided by engineer. Revoke - 撤销 + 吊销 No comment provided by engineer. Revoke file - 撤销文件 + 吊销文件 cancel file action Revoke file? - 撤销文件? + 吊销文件? No comment provided by engineer. @@ -4670,58 +6501,69 @@ Error: %@ Run chat - 运行聊天程序 + 运行聊天 No comment provided by engineer. - - SMP servers + + SMP server SMP 服务器 No comment provided by engineer. + + SOCKS proxy + No comment provided by engineer. + + + Safely receive files + 安全接收文件 + No comment provided by engineer. + Safer groups + 更安全的群组 No comment provided by engineer. Save 保存 - chat item action + alert button +chat item action Save (and notify contacts) 保存(并通知联系人) - No comment provided by engineer. + alert button Save and notify contact 保存并通知联系人 - No comment provided by engineer. + alert button 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 保存密码并打开聊天 @@ -4735,7 +6577,7 @@ Error: %@ Save preferences? 保存偏好设置? - No comment provided by engineer. + alert title Save profile password @@ -4750,20 +6592,21 @@ Error: %@ Save servers? 保存服务器? - No comment provided by engineer. - - - Save settings? - 保存设置? - No comment provided by engineer. + alert title Save welcome message? 保存欢迎信息? No comment provided by engineer. + + Save your profile? + 保存您的个人资料? + alert title + Saved + 已保存 No comment provided by engineer. @@ -4773,6 +6616,7 @@ Error: %@ Saved from + 保存自 No comment provided by engineer. @@ -4780,6 +6624,21 @@ Error: %@ 已保存的消息 message info title + + Saving %lld messages + 正在保存 %lld 条消息 + No comment provided by engineer. + + + Scale + 规模 + No comment provided by engineer. + + + Scan / Paste link + 扫描 / 粘贴链接 + No comment provided by engineer. + Scan QR code 扫描二维码 @@ -4820,11 +6679,21 @@ Error: %@ 搜索或粘贴 SimpleX 链接 No comment provided by engineer. + + Secondary + 二级 + No comment provided by engineer. + Secure queue 保护队列 server test step + + Secured + 担保 + No comment provided by engineer. + Security assessment 安全评估 @@ -4838,6 +6707,20 @@ Error: %@ Select 选择 + chat item action + + + 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. @@ -4875,11 +6758,6 @@ Error: %@ 将送达回执发送给 No comment provided by engineer. - - Send direct message - 发送私信 - No comment provided by engineer. - Send direct message to connect 发送私信来连接 @@ -4890,6 +6768,11 @@ Error: %@ 发送限时消息中 No comment provided by engineer. + + Send errors + 发送错误 + No comment provided by engineer. + Send link previews 发送链接预览 @@ -4900,14 +6783,28 @@ Error: %@ 发送实时消息 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. @@ -4933,7 +6830,7 @@ Error: %@ Sender cancelled file transfer. 发送人已取消文件传输。 - No comment provided by engineer. + alert message Sender may have deleted the connection request. @@ -4990,6 +6887,11 @@ Error: %@ 已发送于:%@ copied message info + + Sent directly + 直接发送 + No comment provided by engineer. + Sent file event 已发送文件项目 @@ -5000,11 +6902,66 @@ Error: %@ 已发信息 message info title + + 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 + No comment provided by engineer. + + + Server added to operator %@. + alert message + + + Server address + 服务器地址 + No comment provided by engineer. + + + Server address is incompatible with network settings. + 服务器地址与网络设置不兼容。 + srv error text. + + + Server address is incompatible with network settings: %@. + 服务器地址与网络设置不兼容:%@。 + No comment provided by engineer. + + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password 服务器需要授权才能创建队列,检查密码 @@ -5020,11 +6977,36 @@ Error: %@ 服务器测试失败! No comment provided by engineer. + + Server type + 服务器类型 + No comment provided by engineer. + + + Server version is incompatible with network settings. + 服务器版本与网络设置不兼容。 + srv error text + + + 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 会话码 @@ -5035,11 +7017,20 @@ Error: %@ 设定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 设置群组偏好设置 @@ -5050,6 +7041,10 @@ Error: %@ 设置它以代替系统身份验证。 No comment provided by engineer. + + Set message expiration in chats. + No comment provided by engineer. + Set passcode 设置密码 @@ -5057,6 +7052,7 @@ Error: %@ Set passphrase + 设置密码短语 No comment provided by engineer. @@ -5079,28 +7075,51 @@ Error: %@ 设置 No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images + 改变个人资料图形状 No comment provided by engineer. Share 分享 - chat item action + alert action +chat item action Share 1-time link 分享一次性链接 No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address 分享地址 No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? 与联系人分享地址? + alert title + + + Share from other apps. + 从其他应用程序共享。 No comment provided by engineer. @@ -5108,18 +7127,32 @@ Error: %@ 分享链接 No comment provided by engineer. + + Share profile + 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. + + Short link + No comment provided by engineer. + Show QR code + 显示二维码 No comment provided by engineer. @@ -5137,21 +7170,46 @@ Error: %@ 显示最近的消息 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 → on messages sent via private routing. + 显示 → 通过专用路由发送的信息. + 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 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 审核。 @@ -5182,6 +7240,21 @@ Error: %@ 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 channel link + SimpleX 频道链接 + simplex link type + SimpleX contact address SimpleX 联系地址 @@ -5189,12 +7262,12 @@ Error: %@ SimpleX encrypted message or connection event - SimpleX 加密消息或连接项目 + SimpleX 加密的消息或连接事件 notification SimpleX group link - SimpleX 群组链接 + SimpleX 群链接 simplex link type @@ -5202,12 +7275,14 @@ Error: %@ SimpleX 链接 chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. + 此群禁止 SimpleX 链接。 No comment provided by engineer. SimpleX links not allowed + 不允许SimpleX 链接 No comment provided by engineer. @@ -5215,11 +7290,21 @@ Error: %@ SimpleX 一次性邀请 simplex link type + + 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 跳过 @@ -5235,18 +7320,49 @@ Error: %@ 小群组(最多 20 人) No comment provided by engineer. + + Soft + + blur media + + + 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: + 导入过程中出现一些非致命错误: + No comment provided by engineer. + + + Some servers failed the test: +%@ + alert message + Somebody 某人 notification title + + Spam + blocking reason +report reason + Square, circle, or anything in between. + 方形、圆形、或两者之间的任意形状. No comment provided by engineer. @@ -5264,6 +7380,16 @@ Error: %@ 开始迁移 No comment provided by engineer. + + Starting from %@. + 从 %@ 开始。 + No comment provided by engineer. + + + Statistics + 统计 + No comment provided by engineer. + Stop 停止 @@ -5276,11 +7402,7 @@ Error: %@ Stop chat - No comment provided by engineer. - - - Stop chat to enable database actions - 停止聊天以启用数据库操作 + 停止聊天程序 No comment provided by engineer. @@ -5311,27 +7433,60 @@ Error: %@ Stop sharing 停止分享 - No comment provided by engineer. + alert action Stop sharing address? 停止分享地址? - No comment provided by engineer. + alert title Stopping chat + 正在停止聊天 No comment provided by engineer. + + Storage + No comment provided by engineer. + + + Strong + 加粗 + blur media + 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 系统 @@ -5342,11 +7497,20 @@ Error: %@ 系统验证 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 + No comment provided by engineer. + TCP_KEEPCNT TCP_KEEPCNT @@ -5362,11 +7526,19 @@ Error: %@ TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture 拍照 No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button 点击按钮 @@ -5402,16 +7574,20 @@ Error: %@ 轻按扫描 No comment provided by engineer. - - Tap to start a new chat - 点击开始一个新聊天 - No comment provided by engineer. + + Temporary file error + 临时文件错误 + file error alert title Test failed at step %@. 在步骤 %@ 上测试失败。 server test failure + + Test notifications + No comment provided by engineer. + Test server 测试服务器 @@ -5425,7 +7601,7 @@ Error: %@ Tests failed! 测试失败! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -5442,11 +7618,6 @@ Error: %@ 感谢用户——通过 Weblate 做出贡献! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - 第一个没有任何用户标识符的平台 - 隐私设计. - No comment provided by engineer. - 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. @@ -5459,6 +7630,15 @@ It can happen because of some bug or when the connection is compromised.该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。 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. 更改数据库密码的尝试未完成。 @@ -5469,6 +7649,10 @@ It can happen because of some bug or when the connection is compromised.您扫描的码不是 SimpleX 链接的二维码。 No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! 您接受的连接将被取消! @@ -5489,6 +7673,11 @@ It can happen because of some bug or when the connection is compromised.加密正在运行,不需要新的加密协议。这可能会导致连接错误! No comment provided by engineer. + + The future of messaging + 下一代私密通讯软件 + No comment provided by engineer. + The hash of the previous message is different. 上一条消息的散列不同。 @@ -5504,9 +7693,14 @@ It can happen because of some bug or when the connection is compromised.该消息将对所有成员标记为已被管理员移除。 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. @@ -5514,9 +7708,12 @@ It can happen because of some bug or when the connection is compromised.旧数据库在迁移过程中没有被移除,可以删除。 No comment provided by engineer. - - The profile is only shared with your contacts. - 该资料仅与您的联系人共享。 + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The second preset operator in the app! No comment provided by engineer. @@ -5534,16 +7731,28 @@ It can happen because of some bug or when the connection is compromised.您当前聊天资料 **%@** 的新连接服务器。 No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. 您粘贴的文本不是 SimpleX 链接。 No comment provided by engineer. - - Theme + + 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 **%@**. 这些设置适用于您当前的配置文件 **%@**。 @@ -5564,6 +7773,10 @@ It can happen because of some bug or when the connection is compromised.此操作无法撤消——早于所选的发送和接收的消息将被删除。 这可能需要几分钟时间。 No comment provided by engineer. + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + alert message + This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. 此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。 @@ -5571,10 +7784,12 @@ It can happen because of some bug or when the connection is compromised. This chat is protected by end-to-end encryption. + 此聊天受端到端加密保护。 E2EE info chat item This chat is protected by quantum resistant end-to-end encryption. + 此聊天受抗量子的端到端加密保护。 E2EE info chat item @@ -5607,11 +7822,29 @@ It can happen because of some bug or when the connection is compromised.这是你自己的一次性链接! 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. + 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: 要提出任何问题并接收更新,请: @@ -5632,9 +7865,8 @@ It can happen because of some bug or when the connection is compromised.建立新连接 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 protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -5642,6 +7874,11 @@ It can happen because of some bug or when the connection is compromised.为了保护时区,图像/语音文件使用 UTC。 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 information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5649,6 +7886,23 @@ You will be prompted to complete authentication before this feature is enabled.< 在启用此功能之前,系统将提示您完成身份验证。 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 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. 请授权使用麦克风以录制语音消息。 @@ -5659,26 +7913,58 @@ You will be prompted to complete authentication before this feature is enabled.< 要显示您的隐藏的个人资料,请在**您的聊天个人资料**页面的搜索字段中输入完整密码。 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. 要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。 No comment provided by engineer. + + Toggle chat list: + 切换聊天列表: + No comment provided by engineer. + Toggle incognito when connecting. 在连接时切换隐身模式。 No comment provided by engineer. + + Token status: %@. + token status + + + 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: %@). 正在尝试连接到用于从该联系人接收消息的服务器(错误:%@)。 @@ -5691,6 +7977,7 @@ You will be prompted to complete authentication before this feature is enabled.< Turkish interface + 土耳其语界面 No comment provided by engineer. @@ -5733,10 +8020,9 @@ You will be prompted to complete authentication before this feature is enabled.< 解封成员吗? No comment provided by engineer. - - Unexpected error: %@ - 意外错误: %@ - item status description + + Undelivered messages + No comment provided by engineer. Unexpected migration state @@ -5746,7 +8032,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. 取消最喜欢 - No comment provided by engineer. + swipe action Unhide @@ -5783,6 +8069,11 @@ You will be prompted to complete authentication before this feature is enabled.< 未知错误 No comment provided by engineer. + + Unknown servers! + 未知服务器! + alert title + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. 除非您使用 iOS 通话界面,否则请启用请勿打扰模式以避免打扰。 @@ -5818,11 +8109,15 @@ To connect, please ask your contact to create another connection link and check Unmute 取消静音 - No comment provided by engineer. + notification label action Unread 未读 + swipe action + + + Unsupported connection link No comment provided by engineer. @@ -5835,11 +8130,6 @@ To connect, please ask your contact to create another connection link and check 更新 No comment provided by engineer. - - Update .onion hosts setting? - 更新 .onion 主机设置? - No comment provided by engineer. - Update database passphrase 更新数据库密码 @@ -5850,9 +8140,13 @@ To connect, please ask your contact to create another connection link and check 更新网络设置? No comment provided by engineer. - - Update transport isolation mode? - 更新传输隔离模式? + + Update settings? + 更新设置? + No comment provided by engineer. + + + Updated conditions No comment provided by engineer. @@ -5860,18 +8154,19 @@ To connect, please ask your contact to create another connection link and check 更新设置会将客户端重新连接到所有服务器。 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 升级并打开聊天 No comment provided by engineer. + + Upload errors + 上传错误 + No comment provided by engineer. + Upload failed + 上传失败了 No comment provided by engineer. @@ -5879,8 +8174,23 @@ To connect, please ask your contact to create another connection link and check 上传文件 server test step + + Uploaded + 已上传 + No comment provided by engineer. + + + Uploaded files + 已上传的文件 + No comment provided by engineer. + Uploading archive + 正在上传存档 + No comment provided by engineer. + + + Use %@ No comment provided by engineer. @@ -5888,11 +8198,23 @@ To connect, please ask your contact to create another connection link and check 使用 .onion 主机 No comment provided by engineer. + + Use SOCKS proxy + No comment provided by engineer. + Use SimpleX Chat servers? 使用 SimpleX Chat 服务器? No comment provided by engineer. + + Use TCP port %@ when no port is specified. + No comment provided by engineer. + + + Use TCP port 443 for preset servers only. + No comment provided by engineer. + Use chat 使用聊天 @@ -5903,6 +8225,14 @@ To connect, please ask your contact to create another connection link and check 使用当前配置文件 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 用于新连接 @@ -5925,6 +8255,17 @@ To connect, please ask your contact to create another connection link and check 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. @@ -5932,18 +8273,35 @@ To connect, please ask your contact to create another connection link and check 使用服务器 No comment provided by engineer. + + Use servers + No comment provided by engineer. + + + Use short links (BETA) + No comment provided by engineer. + Use the app while in the call. + 通话时使用本应用. No comment provided by engineer. - - User profile - 用户资料 + + Use the app with one hand. + 用一只手使用应用程序。 No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - 使用 .onion 主机需要兼容的 VPN 提供商。 + + Use web port + No comment provided by engineer. + + + User selection + 用户选择 + No comment provided by engineer. + + + Username No comment provided by engineer. @@ -5973,10 +8331,12 @@ To connect, please ask your contact to create another connection link and check Verify database passphrase + 验证数据库密码短语 No comment provided by engineer. Verify passphrase + 验证密码短语 No comment provided by engineer. @@ -6014,11 +8374,19 @@ To connect, please ask your contact to create another connection link and check 最大 1gb 的视频和文件 No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code 查看安全码 No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history 可见的历史 @@ -6034,13 +8402,14 @@ To connect, please ask your contact to create another connection link and check 语音信息在此聊天中被禁止。 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. @@ -6055,6 +8424,7 @@ To connect, please ask your contact to create another connection link and check Waiting for desktop... + 正在等待桌面... No comment provided by engineer. @@ -6072,8 +8442,19 @@ To connect, please ask your contact to create another connection link and check 等待视频中 No comment provided by engineer. + + Wallpaper accent + 壁纸装饰 + No comment provided by engineer. + + + Wallpaper background + 壁纸背景 + 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. @@ -6098,6 +8479,7 @@ To connect, please ask your contact to create another connection link and check Welcome message is too long + 欢迎消息太大了 No comment provided by engineer. @@ -6112,11 +8494,11 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. + 连接音频和视频通话时。 No comment provided by engineer. - - When people request to connect, you can accept or reject it. - 当人们请求连接时,您可以接受或拒绝它。 + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -6126,14 +8508,17 @@ To connect, please ask your contact to create another connection link and check 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. @@ -6151,28 +8536,44 @@ To connect, please ask your contact to create another connection link and check 降低了电量使用。 No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + 如果没有 Tor 或 VPN,您的 IP 地址将对文件服务器可见。 + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + 如果没有 Tor 或 VPN,您的 IP 地址将对以下 XFTP 中继可见:%@。 + alert message + Wrong database passphrase 数据库密码错误 No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + 密钥错误或连接未知 - 很可能此连接已被删除。 + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + 密钥错误或文件块地址未知 - 很可能文件已删除。 + file error text + Wrong passphrase! 密码错误! No comment provided by engineer. - - XFTP servers + + XFTP server XFTP 服务器 No comment provided by engineer. - - You - - No comment provided by engineer. - You **must not** use the same database on two devices. + 您 **不得** 在两台设备上使用相同的数据库。 No comment provided by engineer. @@ -6195,8 +8596,13 @@ To connect, please ask your contact to create another connection link and check 您已经连接到 %@。 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. @@ -6206,14 +8612,17 @@ To connect, please ask your contact to create another connection link and check 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. @@ -6224,6 +8633,8 @@ To connect, please ask your contact to create another connection link and check You are already joining the group! Repeat join request? + 您已经加入了这个群组! +重复加入请求? No comment provided by engineer. @@ -6236,11 +8647,25 @@ Repeat join request? 您被邀请加入群组 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 can accept calls from lock screen, without device and app authentication. 您可以从锁屏上接听电话,无需设备和应用程序的认证。 No comment provided by engineer. + + 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 您可以以后创建它 @@ -6258,6 +8683,7 @@ Repeat join request? You can give another try. + 你可以再试一次。 No comment provided by engineer. @@ -6270,11 +8696,20 @@ Repeat join request? 你可以通过设置让它对你的 SimpleX 联系人可见。 No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ 您现在可以给 %@ 发送消息 notification body + + 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. 您可以通过设置来设置锁屏通知预览。 @@ -6290,16 +8725,16 @@ Repeat join request? 您可以与您的联系人分享该地址,让他们与 **%@** 联系。 No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - 您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app 您可以通过应用程序设置/数据库或重新启动应用程序开始聊天 No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + 您仍然可以在聊天列表中查看与 %@的对话。 + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. 您可以通过设置开启 SimpleX 锁定。 @@ -6313,23 +8748,23 @@ Repeat join request? You can view invitation link again in connection details. 您可以在连接详情中再次查看邀请链接。 - No comment provided by engineer. + alert message You can't send messages! 您无法发送消息! 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 decide who can connect. + 你决定谁可以连接。 + No comment provided by engineer. + You have already requested connection via this address! 你已经请求通过此地址进行连接! @@ -6338,11 +8773,8 @@ Repeat join request? You have already requested connection! Repeat connection request? - No comment provided by engineer. - - - You have no chats - 您没有聊天记录 + 您已经请求连接了! +重复连接请求? No comment provided by engineer. @@ -6365,11 +8797,26 @@ Repeat connection request? 你加入了这个群组。连接到邀请组成员。 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. 您需要允许您的联系人发送语音消息,以便您能够发送语音消息。 @@ -6385,6 +8832,10 @@ Repeat connection request? 您发送了群组邀请 No comment provided by engineer. + + You should receive notifications. + token info + You will be connected to group when the group host's device is online, please wait or check later! 您将在组主设备上线时连接到该群组,请稍等或稍后再检查! @@ -6392,6 +8843,7 @@ Repeat connection request? 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. @@ -6419,6 +8871,10 @@ Repeat connection request? 当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。 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. 您将停止接收来自该群组的消息。聊天记录将被保留。 @@ -6439,31 +8895,16 @@ Repeat connection request? 您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人 No comment provided by engineer. - - Your %@ servers - 您的 %@ 服务器 - No comment provided by engineer. - Your ICE servers 您的 ICE 服务器 No comment provided by engineer. - - Your SMP servers - 您的 SMP 服务器 - No comment provided by engineer. - Your SimpleX address 您的 SimpleX 地址 No comment provided by engineer. - - Your XFTP servers - 您的 XFTP 服务器 - No comment provided by engineer. - Your calls 您的通话 @@ -6479,16 +8920,17 @@ Repeat connection request? 您的聊天数据库未加密——设置密码来加密。 No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles 您的聊天资料 No comment provided by engineer. - - 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). - 您的联系人需要在线才能完成连接。 -您可以取消此连接并删除联系人(然后尝试使用新链接)。 + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. No comment provided by engineer. @@ -6506,6 +8948,10 @@ You can cancel this connection and remove the contact (and try later with a new 与您的联系人保持连接。 No comment provided by engineer. + + Your credentials may be sent unencrypted. + No comment provided by engineer. + Your current chat database will be DELETED and REPLACED with the imported one. 您当前的聊天数据库将被删除并替换为导入的数据库。 @@ -6528,6 +8974,7 @@ You can cancel this connection and remove the contact (and try later with a new Your profile + 您的个人资料 No comment provided by engineer. @@ -6535,33 +8982,34 @@ You can cancel this connection and remove the contact (and try later with a new 您的个人资料 **%@** 将被共享。 No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - 您的资料存储在您的设备上并仅与您的联系人共享。 -SimpleX 服务器无法看到您的资料。 + + Your profile is stored on your device and only shared with your contacts. + 该资料仅与您的联系人共享。 No comment provided by engineer. - - Your profile, contacts and delivered messages are stored on your device. - 您的资料、联系人和发送的消息存储在您的设备上。 + + 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 was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your random profile 您的随机资料 No comment provided by engineer. - - Your server - 您的服务器 - No comment provided by engineer. - Your server address 您的服务器地址 No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings 您的设置 @@ -6602,6 +9050,10 @@ SimpleX 服务器无法看到您的资料。 已接受通话 call status + + accepted invitation + chat list item title + admin 管理员 @@ -6609,6 +9061,7 @@ SimpleX 服务器无法看到您的资料。 admins + 管理员 feature role @@ -6623,6 +9076,7 @@ SimpleX 服务器无法看到您的资料。 all members + 所有成员 feature role @@ -6632,6 +9086,16 @@ SimpleX 服务器无法看到您的资料。 and %lld other events + 和 %lld 其他事件 + No comment provided by engineer. + + + archived report + No comment provided by engineer. + + + attempts + 尝试 No comment provided by engineer. @@ -6667,13 +9131,19 @@ SimpleX 服务器无法看到您的资料。 blocked by admin 由管理员封禁 - marked deleted chat item preview text + blocked chat item +marked deleted chat item preview text bold 加粗 No comment provided by engineer. + + call + 呼叫 + No comment provided by engineer. + call error 通话错误 @@ -6777,7 +9247,7 @@ SimpleX 服务器无法看到您的资料。 connecting… 连接中…… - chat list item title + No comment provided by engineer. connection established @@ -6791,6 +9261,7 @@ SimpleX 服务器无法看到您的资料。 contact %1$@ changed to %2$@ + 联系人 %1$@ 已更改为 %2$@ profile update event chat item @@ -6823,10 +9294,16 @@ SimpleX 服务器无法看到您的资料。 time unit + + decryption errors + 解密错误 + No comment provided by engineer. + default (%@) 默认 (%@) - pref value + delete after time +pref value default (no) @@ -6873,6 +9350,11 @@ SimpleX 服务器无法看到您的资料。 重复的消息 integrity error chat item + + duplicates + 副本 + No comment provided by engineer. + e2e encrypted 端到端加密 @@ -6948,13 +9430,14 @@ SimpleX 服务器无法看到您的资料。 错误 No comment provided by engineer. - - event happened - 发生的事 + + expired + 过期 No comment provided by engineer. forwarded + 已转发 No comment provided by engineer. @@ -6982,6 +9465,11 @@ SimpleX 服务器无法看到您的资料。 在您重启应用或改变密码后,iOS钥匙串将被用来安全地存储密码——它将允许接收推送通知。 No comment provided by engineer. + + inactive + 无效 + No comment provided by engineer. + incognito via contact address link 通过联系人地址链接隐身聊天 @@ -7022,6 +9510,11 @@ SimpleX 服务器无法看到您的资料。 邀请您加入群组 %@ group name + + invite + 邀请 + No comment provided by engineer. + invited 已邀请 @@ -7069,6 +9562,7 @@ SimpleX 服务器无法看到您的资料。 member %1$@ changed to %2$@ + 成员 %1$@ 已更改为 %2$@ profile update event chat item @@ -7076,6 +9570,11 @@ SimpleX 服务器无法看到您的资料。 已连接 rcv group event chat item + + message + 消息 + No comment provided by engineer. + message received 消息已收到 @@ -7101,6 +9600,10 @@ SimpleX 服务器无法看到您的资料。 由 %@ 审核 marked deleted chat item preview text + + moderator + member role + months @@ -7109,7 +9612,7 @@ SimpleX 服务器无法看到您的资料。 never 从不 - No comment provided by engineer. + delete after time new message @@ -7140,8 +9643,8 @@ SimpleX 服务器无法看到您的资料。 off 关闭 enabled status - group pref value - time to disappear +group pref value +time to disappear offered %@ @@ -7158,6 +9661,16 @@ SimpleX 服务器无法看到您的资料。 开启 group pref value + + other + 其他 + No comment provided by engineer. + + + other errors + 其他错误 + No comment provided by engineer. + owner 群主 @@ -7165,6 +9678,7 @@ SimpleX 服务器无法看到您的资料。 owners + 所有者 feature role @@ -7172,8 +9686,17 @@ SimpleX 服务器无法看到您的资料。 点对点 No comment provided by engineer. + + pending + No comment provided by engineer. + + + pending approval + No comment provided by engineer. + quantum resistant e2e encryption + 抗量子端到端加密 chat item text @@ -7186,6 +9709,10 @@ SimpleX 服务器无法看到您的资料。 已受到确认…… No comment provided by engineer. + + rejected + No comment provided by engineer. + rejected call 拒接来电 @@ -7216,12 +9743,23 @@ SimpleX 服务器无法看到您的资料。 已将您移除 rcv group event chat item + + requested to connect + chat list item title + saved + 已保存 No comment provided by engineer. saved from %@ + 保存自 %@ + No comment provided by engineer. + + + search + 搜索 No comment provided by engineer. @@ -7249,6 +9787,15 @@ SimpleX 服务器无法看到您的资料。 发送私信 No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + 服务器队列信息: %1$@ + +上次收到的消息: %2$@ + queue info + set new contact address 设置新的联系地址 @@ -7261,6 +9808,7 @@ SimpleX 服务器无法看到您的资料。 standard end-to-end encryption + 标准端到端加密 chat item text @@ -7280,6 +9828,7 @@ SimpleX 服务器无法看到您的资料。 unblocked %@ + 未阻止 %@ rcv group event chat item @@ -7287,11 +9836,21 @@ SimpleX 服务器无法看到您的资料。 未知 connection info + + unknown servers + 未知服务器 + No comment provided by engineer. + unknown status 未知状态 No comment provided by engineer. + + unprotected + 未受保护 + No comment provided by engineer. + updated group profile 已更新的群组资料 @@ -7304,6 +9863,7 @@ SimpleX 服务器无法看到您的资料。 v%@ + v%@ No comment provided by engineer. @@ -7331,6 +9891,11 @@ SimpleX 服务器无法看到您的资料。 通过中继 No comment provided by engineer. + + video + 视频 + No comment provided by engineer. + video call (not e2e encrypted) 视频通话(非端到端加密) @@ -7356,6 +9921,11 @@ SimpleX 服务器无法看到您的资料。 time unit + + when IP hidden + 当 IP 隐藏时 + No comment provided by engineer. + yes @@ -7363,6 +9933,7 @@ SimpleX 服务器无法看到您的资料。 you + No comment provided by engineer. @@ -7377,6 +9948,7 @@ SimpleX 服务器无法看到您的资料。 you blocked %@ + 你阻止了%@ snd group event chat item @@ -7421,6 +9993,7 @@ SimpleX 服务器无法看到您的资料。 you unblocked %@ + 您解封了 %@ snd group event chat item @@ -7437,7 +10010,7 @@ SimpleX 服务器无法看到您的资料。
- +
@@ -7457,6 +10030,7 @@ SimpleX 服务器无法看到您的资料。 SimpleX uses local network access to allow using user chat profile via desktop app on the same network. + SimpleX 使用本地网络访问,允许通过同一网络上的桌面应用程序使用用户聊天配置文件。 Privacy - Local Network Usage Description @@ -7473,7 +10047,7 @@ SimpleX 服务器无法看到您的资料。
- +
@@ -7493,4 +10067,245 @@ SimpleX 服务器无法看到您的资料。
+ +
+ +
+ + + %d new events + notification body + + + From %d chat(s) + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + +
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + 版权所有 © 2024 SimpleX Chat。保留所有权利。 + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + 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 "设置中允许共享。 + No comment provided by engineer. + + +
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 03a108d112..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 @@
%d skipped message(s) - %d錯過了訊息 + 錯過的 %d 則訊息 integrity error chat item @@ -124,17 +124,17 @@ %lld contact(s) selected - %lld 已選擇聯絡人(s) + 已選擇 %lld 個聯絡人 No comment provided by engineer. %lld file(s) with total size of %@ - %lld 檔案(s) 的總共大小為%@ + %lld 個檔案,總共大小 %@ No comment provided by engineer. %lld members - %lld 成員 + %lld 個成員 No comment provided by engineer. @@ -187,23 +187,18 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **新增新的聯絡人**:建立一次性二維碼或連結連接聯絡人。 - No comment provided by engineer. - **Create link / QR code** for your contact to use. **建立連結 / 二維碼** 讓你的聯絡人使用。 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. + + **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). + + **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. @@ -217,8 +212,8 @@ **請注意**:如果你忘記了密碼你將不能再次復原或更改密碼。 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. + + **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. **建議**:裝置權杖和通知都會傳送去 SimpeleX Chat 的通知伺服器,但是不包括訊息內容、大小或傳送者資料。 No comment provided by engineer. @@ -229,7 +224,7 @@ **Warning**: Instant push notifications require passphrase saved in Keychain. - **警告**:即時推送訊息通知需要數據庫的密碼儲存在資料庫中。 + **警告**:即時推送訊息通知需要將數據庫的密碼儲存在資料庫中。 No comment provided by engineer. @@ -314,7 +309,7 @@ About SimpleX Chat - 關於 SimpleX 對話 + 關於 SimpleX Chat No comment provided by engineer. @@ -358,9 +353,9 @@ 使用二維碼掃描以新增伺服器。 No comment provided by engineer. - - Add server… - 新增伺服器… + + Add server + 新增伺服器 No comment provided by engineer. @@ -445,7 +440,7 @@ Allow your contacts to send disappearing messages. - 允許你的聯絡人傳送自動銷毀的訊息。 + 允許您的聯絡人傳送限時訊息。 No comment provided by engineer. @@ -1178,8 +1173,8 @@ 私訊 chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. 私訊群組內的成員於這個群組內是禁用的。 No comment provided by engineer. @@ -1198,8 +1193,8 @@ 自動銷毀訊息已被禁止於此聊天室。 No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. 自動銷毀訊息於這個群組內是禁用的。 No comment provided by engineer. @@ -1623,18 +1618,18 @@ 群組內的成員可以不可逆地刪除訊息。 No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. 群組內的成員可以私訊群組內的成員。 No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. 群組內的成員可以傳送自動銷毀的訊息。 No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. 群組內的成員可以傳送語音訊息。 No comment provided by engineer. @@ -1747,8 +1742,8 @@ 下載圖片需要傳送者上線的時候才能下載圖片,請等待對方上線! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam 不受垃圾郵件和濫用行為影響 No comment provided by engineer. @@ -1869,8 +1864,8 @@ 不可逆地刪除訊息於這個聊天室內是禁用的。 No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. 不可逆地刪除訊息於這個群組內是禁用的。 No comment provided by engineer. @@ -2217,8 +2212,8 @@ We will be adding server redundancy to prevent lost messages. 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. @@ -2277,8 +2272,8 @@ We will be adding server redundancy to prevent lost messages. 使用終端機開啟對話 authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. 開放源碼協議和程式碼 – 任何人也可以運行伺服器。 No comment provided by engineer. @@ -2317,8 +2312,8 @@ We will be adding server redundancy to prevent lost messages. 將你接收到的連結貼上至下面的框內,以開始你與你的聯絡人對話。 No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. 人們只能在你分享了連結後,才能和你連接。 No comment provided by engineer. @@ -2709,12 +2704,12 @@ We will be adding server redundancy to prevent lost messages. Send link previews - 傳送可以預覽的連結 + 傳送連結預覽 No comment provided by engineer. Send live message - 傳送實況的訊息 + 傳送實時訊息 No comment provided by engineer. @@ -2729,7 +2724,7 @@ We will be adding server redundancy to prevent lost messages. Send questions and ideas - 傳送問題和想法給開發者 + 給開發者提問題和想法 No comment provided by engineer. @@ -2779,7 +2774,7 @@ We will be adding server redundancy to prevent lost messages. Set 1 day - 設定為1天 + 設定為 1 天 No comment provided by engineer. @@ -3010,8 +3005,8 @@ We will be adding server redundancy to prevent lost messages. 感謝你安裝SimpleX Chat! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. 第一個沒有任何用戶識別符的通訊平台 – 以私隱為設計。 No comment provided by engineer. @@ -3027,7 +3022,7 @@ We will be adding server redundancy to prevent lost messages. The connection you accepted will be cancelled! - 你所接受的連接將被取消! + 你接受的連接將被取消! No comment provided by engineer. @@ -3049,8 +3044,8 @@ We will be adding server redundancy to prevent lost messages. The microphone does not work when the app is in the background. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging 新一代的私密訊息平台 No comment provided by engineer. @@ -3059,14 +3054,14 @@ We will be adding server redundancy to prevent lost messages. 舊的數據庫在遷移過程中沒有被移除,可以刪除。 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 sender will NOT be notified - 發送者不會接收到通知 + 發送者不會收到通知 No comment provided by engineer. @@ -3076,12 +3071,12 @@ We will be adding server redundancy to prevent lost messages. This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - 這操作不能還原 - 所有已經接收和傳送的檔案和媒體檔案將刪除。低解析度圖片將保留。 + 這操作不能還原 - 將刪除所有已經接收和傳送的檔案和媒體。將保留低解析度圖片。 No comment provided by engineer. This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - 這操作無法撤銷 - 早於所選擇的時間發送和接收的訊息將被刪除。這可能需要幾分鐘的時間。 + 這操作無法撤銷 - 早於所選時間的收發訊息將被刪除。可能需要幾分鐘。 No comment provided by engineer. @@ -3118,8 +3113,8 @@ We will be adding server redundancy to prevent lost messages. To prevent the call interruption, enable Do Not Disturb mode. 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. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. 為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 IDs 資料,SimpleX 平台有自家佇列的標識符,這對於你的每個聯絡人也是獨一無二的。 No comment provided by engineer. @@ -3268,7 +3263,7 @@ To connect, please ask your contact to create another connection link and check Use for new connections - 用於新的連接 + 用於新的連線 No comment provided by engineer. @@ -3288,7 +3283,7 @@ To connect, please ask your contact to create another connection link and check Verify connection security - 驗證連接安全性 + 驗證連線安全性 No comment provided by engineer. @@ -3321,8 +3316,8 @@ To connect, please ask your contact to create another connection link and check 語音訊息於這個聊天窒是禁用的。 No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. 語音訊息於這個群組內是禁用的。 No comment provided by engineer. @@ -3455,11 +3450,6 @@ To connect, please ask your contact to create another connection link and check 你可以使用 Markdown 語法以更清楚標明訊息: 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. 你未能通過認證;請再試一次。 @@ -4173,7 +4163,7 @@ SimpleX 伺服器並不會看到你的個人檔案。 via contact address link - 透過聯絡人的邀請連結連接 + 透過聯絡人的邀請連結連線 chat list item description @@ -4183,7 +4173,7 @@ SimpleX 伺服器並不會看到你的個人檔案。 via one-time link - 透過一次性連結連接 + 透過一次性連結連線 chat list item description @@ -4712,7 +4702,7 @@ Available in v5.1 %u messages failed to decrypt. - %u 訊息解密失敗。 + %u 則訊息解密失敗。 No comment provided by engineer. @@ -4791,8 +4781,8 @@ Available in v5.1 訊息 & 檔案 No comment provided by engineer. - - Migrations: %@ + + Migrations: 遷移:%@ No comment provided by engineer. @@ -5162,7 +5152,7 @@ Available in v5.1 Tap to activate profile. - 點擊以激活配置檔案。 + 點擊以激活設定檔。 No comment provided by engineer. @@ -5523,8 +5513,8 @@ It can happen because of some bug or when the connection is compromised.啟用自毀密碼 set passcode view - - Group members can add message reactions. + + Members can add message reactions. 群組內的成員可以新增訊息互動。 No comment provided by engineer. @@ -5699,8 +5689,8 @@ It can happen because of some bug or when the connection is compromised.已移除在 No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. 訊息互動於這個群組內是禁用的。 No comment provided by engineer. @@ -5898,6 +5888,598 @@ It can happen because of some bug or when the connection is compromised.%@ 和 %@ 已連接 No comment provided by engineer. + + %@ downloaded + %@ 下載 + + + %@ uploaded + %@ 上傳 + + + Abort + 中止 + + + **Create group**: to create a new group. + **創建群組**: 創建一個新的群組。 + + + Abort changing address + 中止更改地址 + + + Accept connection request? + 接受連線請求? + + + Camera not available + 相機不可用 + + + All messages will be deleted - this cannot be undone! + 所有訊息都將被刪除 - 這不能還原! + + + Allow irreversible message deletion only if your contact allows it to you. (24 hours) + 只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。(24小時) + + + Allow to irreversibly delete sent messages. (24 hours) + 允許將不可撤銷的訊息刪除。(24小時) + + + Allow your contacts to irreversibly delete sent messages. (24 hours) + 允許您的聯絡人不可復原地刪除已傳送的訊息。(24小時) + + + Bad desktop address + 無效的桌面地址 + + + Error decrypting file + 解密檔案時出錯 + + + Add contact + 新增聯絡人 + + + Advanced settings + 進階設定 + + + Allow calls? + 允許通話? + + + Allow to send files and media. + 允許傳送檔案和媒體。 + + + Already joining the group! + 已加入群組! + + + App data migration + 應用資料轉移 + + + Apply + 應用 + + + Apply to + 應用到 + + + Archive and upload + 儲存並上傳 + + + Block + 封鎖 + + + Block group members + 封鎖群組成員 + + + Block member + 封鎖成員 + + + 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)! + + + Can't call member + 無法與成員通話 + + + Can't message member + 無法傳送訊息給成員 + + + Cancel migration + 取消遷移 + + + Chat database exported + 導出聊天數據庫 + + + 0 sec + 0 秒 + + + All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + 你的所有聯絡人、對話和文件將被安全加密並切塊上傳到設置的 XFTP 中繼。 + + + Address change will be aborted. Old receiving address will be used. + 將取消地址更改。將使用舊聯絡地址。 + + + Archiving database + 正在儲存資料庫 + + + Cellular + 行動網路 + + + %@, %@ and %lld members + %@, %@ 和 %lld 成員 + + + %lld messages marked deleted + %lld 則訊息已標記為刪除 + + + Already connecting! + 已連接! + + + Block member? + 封鎖成員? + + + (new) + (新) + + + %@, %@ and %lld other members connected + %@, %@ 和 %lld 個成員已連接 + + + A few more things + 其他 + + + Show last messages + 顯示最新的訊息 + + + App encrypts new local files (except videos). + 應用程式將為新的本機文件(影片除外)加密。 + + + Better groups + 更加的群組 + + + %lld new interface languages + %lld 種新的介面語言 + + + Blocked by admin + 由管理員封鎖 + + + Both you and your contact can irreversibly delete sent messages. (24 hours) + 您與您的聯絡人都可以不可復原地删除已傳送的訊息。(24小時) + + + Encrypt local files + 加密本機檔案 + + + - more stable message delivery. +- a bit better groups. +- and more! + - 更穩定的傳送! +- 更好的社群! +- 以及更多! + + + - optionally notify deleted contacts. +- profile names with spaces. +- and more! + - 可選擇通知已刪除的聯絡人 +- 帶空格的共人資料名稱。 +-以及更多! + + + Abort changing address? + 中止更改地址? + + + Allow to send SimpleX links. + 允許傳送 SimpleX 連結。 + + + Background + 後台 + + + SimpleX links not allowed + 不允許 SimpleX 連結 + + + Voice messages not allowed + 不允許語音訊息 + + + The text you pasted is not a SimpleX link. + 您貼在這裡的連結不是 SimpleX 連結。 + + + %d file(s) were deleted. + 已刪除 %d 個檔案。 + + + Reset to app theme + 重設至應用程式主題 + + + Retry + 重試 + + + The uploaded database archive will be permanently removed from the servers. + 上傳的資料庫存檔將從伺服器永久移除。 + + + Shape profile images + 塑造個人資料圖片 + + + **Scan / Paste link**: to connect via a link you received. + **掃描/貼上連結**:以透過您收到的連結連線。 + + + Reports + 舉報 + + + Use SOCKS proxy + 使用 SOCKS 代理 + + + Reset all statistics + 重設所有統計數據 + + + SOCKS proxy + SOCKS 代理 + + + Send message to enable calls. + 發送訊息以啟用通話功能。 + + + Send direct message to connect + 直接發送訊息以連結 + + + Scale + 顯示比例 + + + Sent via proxy + 通過代理發送 + + + Servers info + 伺服器訊息 + + + Set message expiration in chats. + 設定聊天中訊息期限。 + + + Share SimpleX address on social media. + 在社交媒體上分享 SimpleX 聯絡地址。 + + + Storage + 存儲 + + + Starting from %@. + 開始於 %@。 + + + The second tick we missed! ✅ + 我們錯過的第二個勾選! ✅ + + + Themes + 主題 + + + %d file(s) failed to download. + %d 個檔案下載失敗。 + + + Session code + 會話代碼 + + + Servers statistics will be reset - this cannot be undone! + 伺服器統計資料將被重設 - 此操作無法撤銷! + + + **Create 1-time link**: to create and share a new invitation link. + **建立一次性連結**:建立並分享新邀請連結。 + + + Set default theme + 設定缺省主題 + + + %lld group events + %lld 個群組事件 + + + Reset all statistics? + 重設所有統計數據? + + + %@ server + %@ 伺服器 + + + %d file(s) were not downloaded. + %d 個檔案未下載。 + + + %d messages not forwarded + %d 則訊息未轉發 + + + Test notifications + 测试通知 + + + (this device v%@) + (此設備 v%@) + + + Settings were changed. + 設定已更改。 + + + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + 這操作不能撤銷 - 此聊天中早於所選訊息的收發訊息將被刪除。 + + + Subscription errors + 訂閱錯誤 + + + Report + 舉報 + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + 當 IP 位址受保護且您或目的地伺服器不支援私人路由時,直接傳送訊息。 + + + Reset to user theme + 重設為使用者主題 + + + Use short links (BETA) + 使用短連結(Beta) + + + Up to 100 last messages are sent to new members. + 最多 100 則最後的訊息會傳送至新成員。 + + + %d seconds(s) + %d 秒 + + + %d file(s) are still being downloaded. + 仍在下載 %d 個檔案。 + + + %lld messages blocked by admin + %lld 則訊息被管理員封鎖 + + + Report: %@ + 舉報:%@ + + + Review conditions + 檢視使用條款 + + + Search or paste SimpleX link + 搜尋或貼上 SimpleX 連結 + + + Sent directly + 已直接發送 + + + SimpleX links are prohibited. + 這群組禁止 SimpleX 連結。 + + + Uploaded files + 已上傳的檔案 + + + Use %@ + 使用 %@ + + + Upload errors + 上傳錯誤 + + + Use servers + 使用伺服器 + + + security code changed + 安全碼已變更 + + + These settings are for your current profile **%@**. + 這些設定是針對您目前的設定檔 **%@**。 + + + They can be overridden in contact and group settings. + 您可在連絡人和群組設定中覆寫它們。 + + + %1$@, %2$@ + %1$@, %2$@ + + + Verify connections + 驗證連線 + + + Verify connection + 驗證連線 + + + Verify passphrase + 驗證密碼 + + + Verify code with desktop + 使用桌上電腦驗證代碼 + + + Save list + 儲存列表 + + + Saving %lld messages + 正在儲存 %lld 則訊息 + + + search + 搜尋 + + + requested to connect + 已請求連結 + + + saved + 已儲存 + + + video + 視訊 + + + Tap to Connect + 點擊以連結 + + + Unsupported connection link + 未受支持的連線連結 + + + Saved from + 儲存自 + + + Saved + 已儲存 + + + Scan / Paste link + 掃描/貼上連結 + + + SimpleX + SimpleX + + + Use the app while in the call. + 在通話時使用此應用程式。 + + + v%@ + v%@ + + + Save your profile? + 儲存設定檔? + + + Use for messages + 用於訊息 + + + Uploading archive + 正在上傳檔案庫 + + + Unlink + 從桌上電腦解除連結 + + + %lld messages blocked + 已封鎖 %d 則訊息 + + + The same conditions will apply to operator **%@**. + 相同條件也適用於 **%@** 操作員。 + + + These conditions will also apply for: **%@**. + 這些條件也適用於:**%@**。 + + + Upload failed + 上傳失敗 + + + Use the app with one hand. + 單手使用此應用程式。 + + + Safely receive files + 安全地接收檔案 + + + Saved message + 已儲存的訊息 + + + Use from desktop + 在桌上電腦上使用 + + + Via secure quantum resistant protocol. + 使用量子安全的協定。 + + + Uploaded + 已上傳 + 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 faa7f4f44c..176da2481e 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -22,18 +22,53 @@ let nseSuspendSchedule: SuspendSchedule = (2, 4) let fastNSESuspendSchedule: SuspendSchedule = (1, 1) -enum NSENotification { - case nse(UNMutableNotificationContent) - case callkit(RcvCallInvitation) - case empty - case msgInfo(NtfMsgInfo) +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 - 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 } } } @@ -43,19 +78,25 @@ 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 = [] - private var activeThreads: [(UUID, NotificationService)] = [] + 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 } } + @inline(__always) func startThread(_ t: UUID, _ service: NotificationService) { - NSEThreads.queue.sync { + queue.sync { if allThreads.contains(t) { activeThreads.append((t, service)) } else { @@ -64,27 +105,111 @@ class NSEThreads { } } - func processNotification(_ id: ChatId, _ ntf: NSENotification) async -> Void { - var waitTime: Int64 = 5_000_000000 - while waitTime > 0 { - if let (_, nse) = rcvEntityThread(id), - nse.shouldProcessNtf && nse.processReceivedNtf(ntf) { - break + // 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 { - try? await Task.sleep(nanoseconds: 10_000000) - waitTime -= 10_000000 + nil } } } - private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? { - NSEThreads.queue.sync { - activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id }) + // 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 { + queue.sync { let tActive: UUID? = if let index = activeThreads.firstIndex(where: { $0.0 == t }) { activeThreads.remove(at: index).0 } else { @@ -101,53 +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 notificationInfo: NtfMessages? - var receiveEntityId: String? - var expectedMessages: Set = [] - // return true if the message is taken - it prevents sending it to another NotificationService instance for processing - var shouldProcessNtf = false + var notificationEntities: Dictionary = [:] // 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)") 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()) @@ -163,91 +337,250 @@ class NotificationService: UNNotificationServiceExtension { } } logger.debug("NotificationService: app state is now \(state.rawValue)") - if state.inactive { - receiveNtfMessages(request, contentHandler) + 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, self) } + 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))") - if let connEntity = ntfInfo.connEntity_ { - setBestAttemptNtf( - ntfInfo.ntfsEnabled - ? .nse(createConnectionEventNtf(ntfInfo.user, connEntity)) - : .empty - ) - if let id = connEntity.id, ntfInfo.msgTs != nil { - notificationInfo = ntfInfo - receiveEntityId = id - expectedMessages = Set(ntfInfo.ntfMessages.map { $0.msgId }) - shouldProcessNtf = true - return + 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) + } } } } 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) } - func processReceivedNtf(_ ntf: NSENotification) -> Bool { - guard let ntfInfo = notificationInfo, let msgTs = ntfInfo.msgTs else { return false } - if !ntfInfo.user.showNotifications { - self.setBestAttemptNtf(.empty) - } + @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 { - let found = expectedMessages.remove(info.msgId) - if found != nil { - logger.debug("NotificationService processNtf: msgInfo, last: \(self.expectedMessages.isEmpty)") - if expectedMessages.isEmpty { - self.deliverBestAttemptNtf() - } + 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 info.msgTs > msgTs { - logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one") - self.deliverBestAttemptNtf() - return false + } 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 { - logger.debug("NotificationService processNtf: unknown message, let other instance to process it") - return false + // 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 ntfInfo.user.showNotifications { + } else if ntfEntity.ntfConn.user.showNotifications { + // This is the notification event for the user with enabled notifications. logger.debug("NotificationService processNtf: setting best attempt") - self.setBestAttemptNtf(ntf) - if ntf.isCallInvitation { - self.deliverBestAttemptNtf() + 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 } - return false + } + + func finalizeEntity(_ entityId: ChatId) { + if let t = threadId { logger.debug("NotificationService thread \(t): entityReady: entity \(entityId)") } + NSEThreads.shared.signalNextThread(self, entityId) + deliverBestAttemptNtf() } func setBadgeCount() { @@ -255,51 +588,53 @@ class NotificationService: UNNotificationServiceExtension { ntfBadgeCountGroupDefault.set(badgeCount) } - func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) { - setBestAttemptNtf(.nse(ntf)) - } - - func setBestAttemptNtf(_ ntf: NSENotification) { - logger.debug("NotificationService.setBestAttemptNtf") - if case let .nse(notification) = ntf { - notification.badge = badgeCount as NSNumber - bestAttemptNtf = .nse(notification) - } else { - bestAttemptNtf = ntf - } + @inline(__always) + func setServiceBestAttemptNtf(_ ntf: UNMutableNotificationContent) { + logger.debug("NotificationService.setServiceBestAttemptNtf") + serviceBestAttemptNtf = ntf } private func deliverBestAttemptNtf(urgent: Bool = false) { - logger.debug("NotificationService.deliverBestAttemptNtf") - // stop processing other messages - shouldProcessNtf = 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 + 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) } - deliverCallkitOrNotification(urgent: urgent, suspend: suspend) } - private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) { - if case .callkit = bestAttemptNtf { + @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 { - // suspending NSE even though there may be other notifications - // to allow the app to process callkit call suspendChat(0) - deliverNotification() + deliverNotification(handler, callInv) } else { - // suspending NSE with delay and delivering after the suspension + // when not "urgent", suspending NSE with delay and delivering after the suspension // because pushkit notification must be processed without delay - // to avoid app termination + // to avoid app termination. DispatchQueue.global().asyncAfter(deadline: .now() + fastNSESuspendSchedule.delay) { suspendChat(fastNSESuspendSchedule.timeout) DispatchQueue.global().asyncAfter(deadline: .now() + Double(fastNSESuspendSchedule.timeout)) { - self.deliverNotification() + self.deliverNotification(handler, callInv) } } } @@ -318,38 +653,119 @@ class NotificationService: UNNotificationServiceExtension { } } } - deliverNotification() + deliverNotification(handler, callInv) } } - private func deliverNotification() { - 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 + 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 { - UNMutableNotificationContent() + handler(createCallInvitationNtf(callInv, badgeCount)) } - handler(useNtf) + } 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() + var chatsNames: [String] = [] + var userId: Int64? + for ntf in ntfs { switch ntf { - case let .nse(content): deliver(content) - case let .callkit(invitation): - logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)") - CXProvider.reportNewIncomingVoIPPushPayload([ - "displayName": invitation.contact.displayName, - "contactId": invitation.contact.id, - "media": invitation.callType.media.rawValue - ]) { error in - logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") - deliver(error == nil ? nil : createCallInvitationNtf(invitation)) + case let .messageReceived(user, chat, _): + if seenChatIds.isEmpty { userId = user.userId } + if !seenChatIds.contains(chat.id) { + seenChatIds.insert(chat.id) + chatsNames.append(chat.chatViewName) } - 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 + 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" + : "" + } } } @@ -358,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) @@ -368,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) @@ -392,6 +807,16 @@ func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber } } +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) @@ -406,7 +831,7 @@ func startChat() -> DBMigrationResult? { startLock.wait() defer { startLock.signal() } - + if hasChatCtrl() { return switch NSEChatState.shared.value { case .created: doStartChat() @@ -436,11 +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 apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path) try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) // prevent suspension while starting chat suspendLock.wait() @@ -530,12 +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 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: () } } @@ -545,36 +975,46 @@ func receiveMessages() async { } } -func chatRecvMsg() async -> ChatResponse? { +func chatRecvMsg() async -> APIResult? { await withCheckedContinuation { cont in - let resp = recvSimpleXMsg() + let resp: APIResult? = 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)? { +@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) ?? 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 @@ -588,24 +1028,16 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { 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 - case let .chatError(_, err): - logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))") - return nil - default: - logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)") - return nil } } +@inline(__always) func updateNetCfg() { let newNetConfig = getNetCfg() if newNetConfig != networkConfig { @@ -620,14 +1052,14 @@ func updateNetCfg() { } func apiGetActiveUser() -> User? { - let r = sendSimpleXCmd(.showActiveUser) + let r: APIResult = sendSimpleXCmd(NSEChatCommand.showActiveUser) logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)") switch r { - case let .activeUser(user): return user - case .chatCmdError(_, .error(.noActiveUser)): + case let .result(.activeUser(user)): return user + case .error(.error(.noActiveUser)): logger.debug("apiGetActiveUser sendSimpleXCmd no active user") return nil - case let .chatCmdError(_, err): + case let .error(err): logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))") return nil default: @@ -637,75 +1069,93 @@ func apiGetActiveUser() -> User? { } func apiStartChat() throws -> Bool { - let r = sendSimpleXCmd(.startChat(mainApp: false)) + let r: APIResult = 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 = 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 = 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 apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { + let r: APIResult = 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 = 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 = 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 { - logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessages.count)") - 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) \(String.init(describing: r))") +// logger.debug("NotificationService apiGetConnNtfMessages command: \(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs).cmdString)") + logger.debug("NotificationService apiGetConnNtfMessages requests: \(connMsgReqs.count)") + let r: APIResult = 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 = 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 = 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) -> ChatItem? { @@ -722,18 +1172,32 @@ func autoReceiveFile(_ file: CIFile) -> ChatItem? { } func setNetworkConfig(_ cfg: NetCfg) throws { - let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg)) - if case .cmdOk = r { return } - throw r + let r: APIResult = 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/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/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 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsAttachmentsWithMinCount + 0 + NSExtensionActivationSupportsAttachmentsWithMaxCount + 1 + NSExtensionActivationSupportsWebPageWithMaxCount + 1 + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsFileWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + ShareViewController + + + 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 = 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 = 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 = 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 = 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 = sendSimpleXCmd(SEChatCommand.apiSetEncryptLocalFiles(enable: enable)) + if case .result(.cmdOk) = r { return } + throw r.unexpected +} + +func apiGetChats(userId: User.ID) throws -> Array { + let r: APIResult = 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 = 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 = sendSimpleXCmd(SEChatCommand.apiActivateChat(restoreChat: false)) + if case .result(.cmdOk) = r { return } + throw r.unexpected +} + +func apiSuspendChat(expired: Bool) { + let r: APIResult = 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? = recvSimpleXMsg(messageTimeout: 3_500000) + switch msg { + case .result(.chatSuspended): + suspended = false + break + default: continue + } + } + } + if !suspended { + let _r1: APIResult = 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()) { 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, 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? = 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 { + 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? { + 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(@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 { + 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 @@ + + + + + com.apple.security.application-groups + + group.chat.simplex.app + + keychain-access-groups + + $(AppIdentifierPrefix)chat.simplex.app + + + 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 30345d6895..bb45f57992 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.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 */; }; @@ -101,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 */; }; @@ -115,11 +113,6 @@ 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 */; }; - 5CE0E8AB2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */; }; - 5CE0E8AC2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */; }; - 5CE0E8AD2BF0C1B5008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */; }; - 5CE0E8AE2BF0C1B5008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A92BF0C1B5008D6E06 /* libffi.a */; }; - 5CE0E8AF2BF0C1B5008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */; }; 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; }; @@ -142,25 +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 */; }; 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 */; }; @@ -179,24 +172,78 @@ 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 */ @@ -228,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 */ @@ -248,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"; @@ -267,7 +329,6 @@ 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = ""; }; 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; - 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; 5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; @@ -390,7 +451,7 @@ 5CB634AC29E46CF70066AD6B /* LocalAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthView.swift; sourceTree = ""; }; 5CB634AE29E4BB7D0066AD6B /* SetAppPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAppPasscodeView.swift; sourceTree = ""; }; 5CB634B029E5EFEA0066AD6B /* PasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeView.swift; sourceTree = ""; }; - 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; wrapsLines = 0; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = ""; }; 5CBD285529565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -420,11 +481,6 @@ 5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = ""; }; 5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; 5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; - 5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a"; sourceTree = ""; }; - 5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a"; sourceTree = ""; }; - 5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CE0E8A92BF0C1B5008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -438,24 +494,24 @@ 5CE6C7B42AAB1527007F345C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; - 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = ""; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = ""; }; 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = ""; }; 5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = ""; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; - 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = ""; }; 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 = ""; }; 640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = ""; }; + 640743602CD360E600158442 /* ChooseServerOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseServerOperators.swift; sourceTree = ""; }; 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = ""; }; 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; + 642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; + 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; }; 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; }; - 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = ""; }; 64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = ""; }; 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = ""; }; 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = ""; }; @@ -475,23 +531,122 @@ 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; }; 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = ""; }; 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; + 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 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 = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU.a"; sourceTree = ""; }; + 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = ""; }; 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = ""; }; + 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = ""; }; 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = ""; }; - 8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = ""; }; + 8C01E9BF2C8EFBB6008A4B0A /* objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = objc.h; sourceTree = ""; }; + 8C01E9C02C8EFC33008A4B0A /* objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = objc.m; sourceTree = ""; }; 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; + 8C74C3EB2C1B92A900039E77 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = ""; }; 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = ""; }; 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = ""; }; + 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTypes.swift; sourceTree = ""; }; + 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = ""; }; + 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaperTypes.swift; sourceTree = ""; }; 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDevicePicker.swift; sourceTree = ""; }; + 8C852B072C1086D100BA61E8 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; + 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; + 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = ""; }; + 8CAD466E2D15A8100078D18F /* ChatScrollHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatScrollHelpers.swift; sourceTree = ""; }; + 8CAEF14F2D11A6A000240F00 /* ChatItemsLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemsLoader.swift; sourceTree = ""; }; + 8CB15E9F2CFDA30600C28209 /* ChatItemsMerger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemsMerger.swift; sourceTree = ""; }; + 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionsWebView.swift; sourceTree = ""; }; + 8CBC14852D357CDB00BBD901 /* StorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageView.swift; sourceTree = ""; }; + 8CC317432D4FEB9B00292A20 /* EndlessScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndlessScrollView.swift; sourceTree = ""; }; + 8CC317452D4FEBA800292A20 /* ScrollViewCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewCells.swift; sourceTree = ""; }; 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = ""; }; 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; + 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = ""; }; + B70A39722D24090D00E80A5F /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = ""; }; + B70CE9E52D4BE5930080F36D /* GroupMentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMentions.swift; sourceTree = ""; }; + B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = ""; }; + B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = ""; }; + B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCreationCard.swift; sourceTree = ""; }; + CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = ""; }; + CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = ""; }; + CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = ""; }; + CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = ""; }; + CE7548092C622630009579B7 /* SwipeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeLabel.swift; sourceTree = ""; }; + CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = ""; }; + CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = ""; }; + CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyScrollView.swift; sourceTree = ""; }; + CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = ""; }; + 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 = ""; }; + CEE723AE2C3BD3D70009AE93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX SE.entitlements"; sourceTree = ""; }; + CEE723EF2C3D25C70009AE93 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = ""; }; + CEE723F12C3D25ED0009AE93 /* ShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareModel.swift; sourceTree = ""; }; + CEFB2EDE2CA1BCC7004B1ECE /* SheetRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetRepresentable.swift; sourceTree = ""; }; D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = ""; }; 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 = ""; }; + E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + E5DCF9742C590276007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9752C590277007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9762C590278007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9772C590279007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9782C590279007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9792C59027A007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97A2C59027A007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97B2C59027B007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97C2C59027B007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97D2C59027C007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97E2C59027C007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97F2C59027D007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9802C59027D007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9812C59027D007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9832C5902CE007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9852C5902D4007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9862C5902D5007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + E5DCF9872C5902D8007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9882C5902DC007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9892C5902DC007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98A2C5902DD007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98B2C5902DD007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98C2C5902DE007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98D2C5902DE007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98E2C5902E0007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98F2C5902E0007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9902C5902E1007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9912C5902E1007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9922C5902E2007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9932C5902E2007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9942C5902E3007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9952C59067B007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9972C5906FF007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9992C59072A007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99A2C59072B007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; + E5DCF99B2C59072B007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99C2C59072C007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99D2C59072D007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99E2C59072E007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99F2C59072E007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A02C59072F007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A12C59072F007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A22C590730007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A32C590730007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A42C590731007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A52C590731007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAPITypes.swift; sourceTree = ""; }; + E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEAPITypes.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -500,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 */, @@ -530,12 +688,21 @@ buildActionMask = 2147483647; files = ( 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CE0E8AE2BF0C1B5008D6E06 /* libffi.a in Frameworks */, - 5CE0E8AD2BF0C1B5008D6E06 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CE0E8AC2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a in Frameworks */, - 5CE0E8AB2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a in Frameworks */, - 5CE0E8AF2BF0C1B5008D6E06 /* 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; }; @@ -562,6 +729,7 @@ 5C2E260D27A30E2400F70299 /* Views */ = { isa = PBXGroup; children = ( + B76E6C2F2C5C41C300EC11AA /* Contacts */, 5CB0BA8C282711BC00B3292C /* Onboarding */, 3C714775281C080100CB4D4B /* Call */, 5C971E1F27AEBF7000C8A3CE /* Helpers */, @@ -581,6 +749,8 @@ 5C5F4AC227A5E9AF00B51EF1 /* Chat */ = { isa = PBXGroup; children = ( + 8CC317452D4FEBA800292A20 /* ScrollViewCells.swift */, + 8CC317432D4FEB9B00292A20 /* EndlessScrollView.swift */, 6440CA01288AEC770062C672 /* Group */, 5CE4407427ADB657007B033A /* ChatItem */, 5CEACCE527DE977C000BD591 /* ComposeMessage */, @@ -594,6 +764,10 @@ 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */, 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */, + 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */, + 8CB15E9F2CFDA30600C28209 /* ChatItemsMerger.swift */, + 8CAEF14F2D11A6A000240F00 /* ChatItemsLoader.swift */, + 8CAD466E2D15A8100078D18F /* ChatScrollHelpers.swift */, ); path = Chat; sourceTree = ""; @@ -601,11 +775,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CE0E8A92BF0C1B5008D6E06 /* libffi.a */, - 5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */, - 5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */, - 5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */, - 5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.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 = ""; @@ -625,6 +799,7 @@ 5C764E87279CBC8E000C6508 /* Model */ = { isa = PBXGroup; children = ( + E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */, 5C764E88279CBCB3000C6508 /* ChatModel.swift */, 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, @@ -633,7 +808,6 @@ 5CF937212B25034A00E1D781 /* NSESubscriber.swift */, 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */, 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */, - 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */, ); path = Model; @@ -651,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 = ""; @@ -674,6 +854,7 @@ 5C764E5C279C70B7000C6508 /* Libraries */, 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, + CEE723A82C3BD3D70009AE93 /* SimpleX SE */, 5CA059DA279559F40002BEB4 /* Tests iOS */, 5CE2BA692845308900EC33A6 /* SimpleXChat */, 5CA059CB279559F40002BEB4 /* Products */, @@ -684,6 +865,7 @@ 5CA059C2279559F40002BEB4 /* Shared */ = { isa = PBXGroup; children = ( + 8C74C3E92C1B909200039E77 /* Theme */, 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */, 5C36027227F47AD5009F19D9 /* AppDelegate.swift */, 5CA059C4279559F40002BEB4 /* ContentView.swift */, @@ -703,6 +885,7 @@ 5CA059D7279559F40002BEB4 /* Tests iOS.xctest */, 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */, 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */, + CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */, ); name = Products; sourceTree = ""; @@ -727,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 = ""; @@ -764,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 */, @@ -775,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 */, @@ -787,6 +968,7 @@ 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */, 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */, 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */, + 8CBC14852D357CDB00BBD901 /* StorageView.swift */, ); path = UserSettings; sourceTree = ""; @@ -802,6 +984,9 @@ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */, 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */, 18415835CBD939A9ABDC108A /* UserPicker.swift */, + 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */, + E51CC1E52C62085600DB91FE /* OneHandUICard.swift */, + B70A39722D24090D00E80A5F /* TagListView.swift */, ); path = ChatList; sourceTree = ""; @@ -810,9 +995,11 @@ isa = PBXGroup; children = ( 5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */, + E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */, 5CDCAD472818589900503DA2 /* NotificationService.swift */, 5CDCAD492818589900503DA2 /* Info.plist */, 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */, + E5DCF9822C5902CE007928CC /* Localizable.strings */, ); path = "SimpleX NSE"; sourceTree = ""; @@ -820,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 */, @@ -837,6 +1028,8 @@ 5CE2BA96284537A800EC33A6 /* dummy.m */, 5CD67B8D2B0E858A00C510B1 /* hs_init.h */, 5CD67B8E2B0E858A00C510B1 /* hs_init.c */, + 8C01E9BF2C8EFBB6008A4B0A /* objc.h */, + 8C01E9C02C8EFC33008A4B0A /* objc.m */, ); path = SimpleXChat; sourceTree = ""; @@ -891,7 +1084,6 @@ isa = PBXGroup; children = ( 5C4B3B09285FB130003915F2 /* DatabaseView.swift */, - 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */, 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */, 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */, 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */, @@ -899,6 +1091,21 @@ path = Database; sourceTree = ""; }; + 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 = ""; + }; 6440CA01288AEC770062C672 /* Group */ = { isa = PBXGroup; children = ( @@ -909,10 +1116,20 @@ 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */, 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */, 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */, + B70CE9E52D4BE5930080F36D /* GroupMentions.swift */, ); path = Group; sourceTree = ""; }; + 8C74C3E92C1B909200039E77 /* Theme */ = { + isa = PBXGroup; + children = ( + 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */, + 8C74C3EB2C1B92A900039E77 /* Theme.swift */, + ); + path = Theme; + sourceTree = ""; + }; 8C7D94982B8894D300B7B9E1 /* Migration */ = { isa = PBXGroup; children = ( @@ -922,6 +1139,40 @@ path = Migration; sourceTree = ""; }; + 8C86EBE32C0DAE3700E12243 /* Theme */ = { + isa = PBXGroup; + children = ( + 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */, + 8C852B072C1086D100BA61E8 /* Color.swift */, + 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */, + ); + path = Theme; + sourceTree = ""; + }; + B76E6C2F2C5C41C300EC11AA /* Contacts */ = { + isa = PBXGroup; + children = ( + B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */, + ); + path = Contacts; + sourceTree = ""; + }; + 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 = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -931,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; @@ -953,6 +1205,7 @@ dependencies = ( 5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */, 5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */, + CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */, ); name = "SimpleX (iOS)"; packageProductDependencies = ( @@ -960,6 +1213,9 @@ D77B92DB2952372200A5A1CC /* SwiftyGif */, D7F0E33829964E7E0068AF69 /* LZString */, D7197A1729AE89660055C05A /* WebRTC */, + 8C8118712C220B5B00E6FC94 /* Yams */, + 8CB3476B2CF5CFFA006787A5 /* Ink */, + B728945A2D0C62BF00F7A19A /* ElegantEmojiPicker */, ); productName = "SimpleX (iOS)"; productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */; @@ -1016,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 */ @@ -1028,7 +1303,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1330; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1340; ORGANIZATIONNAME = "SimpleX Chat"; TargetAttributes = { @@ -1048,6 +1323,9 @@ CreatedOnToolsVersion = 13.3; LastSwiftMigration = 1330; }; + CEE723A62C3BD3D70009AE93 = { + CreatedOnToolsVersion = 15.4; + }; }; }; buildConfigurationList = 5CA059C1279559F40002BEB4 /* Build configuration list for PBXProject "SimpleX" */; @@ -1080,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 = ""; @@ -1088,6 +1369,7 @@ 5CA059C9279559F40002BEB4 /* SimpleX (iOS) */, 5CA059D6279559F40002BEB4 /* Tests iOS */, 5CDCAD442818589900503DA2 /* SimpleX NSE */, + CEE723A62C3BD3D70009AE93 /* SimpleX SE */, 5CE2BA672845308900EC33A6 /* SimpleXChat */, ); }; @@ -1117,6 +1399,7 @@ buildActionMask = 2147483647; files = ( 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */, + E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1127,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 */ @@ -1135,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 */, + 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 */, @@ -1149,9 +1445,10 @@ 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 */, @@ -1166,17 +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 */, 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */, - 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.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 */, @@ -1184,8 +1483,10 @@ 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 */, 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 */, @@ -1208,39 +1509,46 @@ 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 */, - 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 */, @@ -1248,21 +1556,26 @@ 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 */, + 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 */, @@ -1275,11 +1588,14 @@ 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; }; @@ -1297,6 +1613,7 @@ buildActionMask = 2147483647; files = ( 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */, + E5DDBE702DC4217900A0EFF0 /* NSEAPITypes.swift in Sources */, 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1306,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 */ @@ -1346,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 */ @@ -1368,6 +1714,7 @@ 5C5B67932ABAF56000DA9412 /* bg */, 5C245F3E2B501F13001CC39F /* tr */, 5C371E502BA9AB6400100AD3 /* hu */, + E5DCF9952C59067B007928CC /* en */, ); name = InfoPlist.strings; sourceTree = ""; @@ -1419,6 +1766,78 @@ name = "SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; + 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 = ""; + }; + 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 = ""; + }; + 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 = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -1552,7 +1971,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 285; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1577,7 +1996,8 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 5.7.3; + MARKETING_VERSION = 6.3.7; + OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1601,7 +2021,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 285; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1626,7 +2046,8 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.3; + MARKETING_VERSION = 6.3.7; + OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1642,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; @@ -1662,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; @@ -1687,7 +2108,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1702,7 +2123,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.3; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1724,7 +2145,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1739,7 +2160,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.3; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1761,7 +2182,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 285; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1787,7 +2208,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.3; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1812,7 +2233,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 215; + CURRENT_PROJECT_VERSION = 285; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1838,7 +2259,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.3; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1856,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 */ @@ -1904,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 */ @@ -1911,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" */ = { @@ -1927,8 +2449,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kirualex/SwiftyGif"; requirement = { - branch = master; - kind = branch; + kind = revision; + revision = 5e8619335d394901379c9add5c4c1c2f420b3800; }; }; D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */ = { @@ -1947,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 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index e2f4adc60f..0dd3483fd7 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -46,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) @@ -63,7 +63,7 @@ public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation 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(fromCString(cjson)), temporaryController) + return (dbMigrationResult(dataFromCString(cjson)), temporaryController) } public func chatInitControllerRemovingDatabases() { @@ -110,27 +110,42 @@ public func resetChatCtrl() { migrationResult = nil } -public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> ChatResponse { - var c = cmd.cmdString.cString(using: .utf8)! - let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)! - return chatResponse(fromCString(cjson)) +@inline(__always) +public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> APIResult { + 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(_ ctrl: chat_ctrl? = nil) -> ChatResponse? { - if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), MESSAGE_TIMEOUT) { - let s = fromCString(cjson) - return s == "" ? nil : chatResponse(s) +@inline(__always) +public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> APIResult? { + 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 @@ -154,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 @@ -171,67 +186,34 @@ struct ParsedServerAddress: Decodable { var parseError: String } +@inline(__always) public func fromCString(_ c: UnsafeMutablePointer) -> 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) -> 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 { - return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) - } - } else if type == "chatError" { - if let jError = jResp["chatError"] as? NSDictionary { - return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) - } - } - } - json = prettyJSON(j) - } - return ChatResponse.response(type: type ?? "invalid", json: json ?? s) } -private func decodeUser_(_ jDict: NSDictionary) -> UserRef? { +@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 { @@ -239,10 +221,23 @@ private func decodeUser_(_ jDict: NSDictionary) -> UserRef? { } } -func parseChatData(_ jChat: Any) throws -> ChatData { +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) { @@ -251,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(_ obj: Any) throws -> T { +@inline(__always) +public func decodeObject(_ obj: Any) throws -> T { try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj)) } +@inline(__always) func decodeProperty(_ obj: Any, _ prop: NSString) -> T? { if let jProp = (obj as? NSDictionary)?[prop] { return try? decodeObject(jProp) @@ -268,28 +265,52 @@ func decodeProperty(_ 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 { @@ -328,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 3aa610e4af..b8d2361ac8 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -8,1196 +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(mainApp: Bool) - case apiStopChat - case apiActivateChat(restoreChat: Bool) - case apiSuspendChat(timeoutMicroseconds: Int) - case setTempFolder(tempFolder: String) - case setFilesFolder(filesFolder: 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 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 apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) - 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 apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) - 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 apiBlockMemberForAll(groupId: Int64, memberId: Int64, blocked: Bool) - 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 apiSetNetworkInfo(networkInfo: UserNetworkInfo) - 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) - case apiUploadStandaloneFile(userId: Int64, file: CryptoFile) - case apiDownloadStandaloneFile(userId: Int64, url: String, file: CryptoFile) - case apiStandaloneFileInfo(url: String) - // 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(mainApp): return "/_start main=\(onOff(mainApp))" - 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 .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 .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 .apiCreateChatItem(noteFolderId, file, mc): - let msg = encodeJSON(ComposedMessage(fileSource: file, msgContent: mc)) - return "/_create *\(noteFolderId) 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 let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId): return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId)" - 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 .apiBlockMemberForAll(groupId, memberId, blocked): return "/_block #\(groupId) \(memberId) blocked=\(onOff(blocked))" - 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 let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))" - 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 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 .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 .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 .apiGetChats: return "apiGetChats" - case .apiGetChat: return "apiGetChat" - case .apiGetChatItemInfo: return "apiGetChatItemInfo" - case .apiSendMessage: return "apiSendMessage" - case .apiCreateChatItem: return "apiCreateChatItem" - case .apiUpdateChatItem: return "apiUpdateChatItem" - case .apiDeleteChatItem: return "apiDeleteChatItem" - case .apiConnectContactViaAddress: return "apiConnectContactViaAddress" - case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem" - case .apiChatItemReaction: return "apiChatItemReaction" - case .apiForwardChatItem: return "apiForwardChatItem" - 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 .apiBlockMemberForAll: return "apiBlockMemberForAll" - 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 .apiSetNetworkInfo: return "apiSetNetworkInfo" - 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 .apiUploadStandaloneFile: return "apiUploadStandaloneFile" - case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" - case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" - 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: 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)) - case let .testStorageEncryption(key): - return .testStorageEncryption(key: obfuscate(key)) - 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(_ d: Data) -> APIResult { +// print("decodeAPIResult \(String(describing: R.self))") + do { +// return try withStackSizeLimit { try jsonDecoder.decode(APIResult.self, from: d) } + return try jsonDecoder.decode(APIResult.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 + } 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(_ f: @escaping () throws -> T) throws -> T { +// let semaphore = DispatchSemaphore(value: 0) +// var result: Result? +// 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) - 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 memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool) - case memberBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, blocked: Bool) - 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 standaloneFileInfo(fileMeta: MigrationFileLinkData?) - case rcvStandaloneFileCreated(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 rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer) - case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) - case rcvFileError(user: UserRef, chatItem_: AChatItem?, 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 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 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 sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) - case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) - // 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, ntfServer: String) - 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) - // pq - case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool) - // 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]) - case appSettings(appSettings: AppSettings) - - 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 .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 .memberBlockedForAll: return "memberBlockedForAll" - case .memberBlockedForAllUser: return "memberBlockedForAllUser" - 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 .standaloneFileInfo: return "standaloneFileInfo" - case .rcvStandaloneFileCreated: return "rcvStandaloneFileCreated" - case .rcvFileStart: return "rcvFileStart" - case .rcvFileProgressXFTP: return "rcvFileProgressXFTP" - case .rcvFileComplete: return "rcvFileComplete" - case .rcvStandaloneFileComplete: return "rcvStandaloneFileComplete" - 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 .sndStandaloneFileCreated: return "sndStandaloneFileCreated" - case .sndFileStartXFTP: return "sndFileStartXFTP" - case .sndFileProgressXFTP: return "sndFileProgressXFTP" - case .sndFileRedirectStartXFTP: return "sndFileRedirectStartXFTP" - case .sndFileRcvCancelled: return "sndFileRcvCancelled" - case .sndFileCompleteXFTP: return "sndFileCompleteXFTP" - case .sndStandaloneFileComplete: return "sndStandaloneFileComplete" - case .sndFileCancelledXFTP: return "sndFileCancelledXFTP" - 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 .contactPQEnabled: return "contactPQEnabled" - case .versionInfo: return "versionInfo" - case .cmdOk: return "cmdOk" - case .chatCmdError: return "chatCmdError" - case .chatError: return "chatError" - case .archiveImported: return "archiveImported" - case .appSettings: return "appSettings" - } - } - } - - 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 .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 .memberBlockedForAll(u, groupInfo, byMember, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nblocked: \(blocked)") - case let .memberBlockedForAllUser(u, groupInfo, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nblocked: \(blocked)") - 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 .standaloneFileInfo(fileMeta): return String(describing: fileMeta) - case .rcvStandaloneFileCreated: 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 .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 .sndStandaloneFileCreated: return noDetails - case let .sndFileStartXFTP(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 .sndFileCancelledXFTP(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, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)" - 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 .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)") - 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) - case let .appSettings(appSettings): return String(describing: appSettings) - } - } - } - - 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 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] @@ -1242,44 +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 rcvConcurrency: Int // pool size - public var tcpKeepAlive: KeepAliveOpts? + 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: 25_000_000, tcpTimeout: 15_000_000, tcpTimeoutPerKb: 10_000, rcvConcurrency: 12, - tcpKeepAlive: KeepAliveOpts.defaults, - smpPingInterval: 1200_000_000, - smpPingCount: 3, - logTLSErrors: false + smpPingInterval: 1200_000_000 ) - public static let proxyDefaults: NetCfg = NetCfg( - socksProxy: nil, - sessionMode: TransportSessionMode.user, + static let proxyDefaults: NetCfg = NetCfg( tcpConnectTimeout: 35_000_000, tcpTimeout: 20_000_000, tcpTimeoutPerKb: 15_000, rcvConcurrency: 8, - tcpKeepAlive: KeepAliveOpts.defaults, - smpPingInterval: 1200_000_000, - smpPingCount: 3, - logTLSErrors: false + 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 } } @@ -1289,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 @@ -1325,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 { @@ -1347,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 @@ -1404,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] @@ -1434,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" } @@ -1483,93 +601,11 @@ 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, Codable { case hidden case contact @@ -1588,47 +624,6 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable { 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(_ json: String) -> T? { if let data = json.data(using: .utf8) { return try? jsonDecoder.decode(T.self, from: data) @@ -1645,16 +640,29 @@ private func encodeCJSON(_ 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) @@ -1674,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) @@ -1692,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) @@ -1709,13 +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 forwardNoFile case invalidChatItemUpdate case invalidChatItemDelete case hasCurrentCall @@ -1730,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) @@ -1793,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) @@ -1803,17 +810,18 @@ 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) @@ -1822,7 +830,7 @@ public enum AgentErrorType: Decodable { case INACTIVE } -public enum CommandErrorType: Decodable { +public enum CommandErrorType: Decodable, Hashable { case PROHIBITED case SYNTAX case NO_CONN @@ -1830,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 @@ -1838,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 @@ -1847,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 @@ -1875,7 +912,13 @@ public enum XFTPErrorType: Decodable { 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 @@ -1893,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 @@ -1902,22 +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 @@ -1926,12 +970,12 @@ 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 @@ -1945,174 +989,14 @@ public enum RemoteCtrlError: Decodable { case protocolError } -public struct MigrationFileLinkData: Codable { - let networkConfig: NetworkConfig? - - public init(networkConfig: NetworkConfig) { - self.networkConfig = networkConfig - } - - public struct NetworkConfig: Codable { - let socksProxy: String? - let hostMode: HostMode? - let requiredHostMode: Bool? - - public init(socksProxy: String?, hostMode: HostMode?, requiredHostMode: Bool?) { - self.socksProxy = socksProxy - self.hostMode = hostMode - self.requiredHostMode = requiredHostMode - } - - public func transformToPlatformSupported() -> NetworkConfig { - return if let hostMode, let requiredHostMode { - NetworkConfig( - socksProxy: nil, - hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode, - requiredHostMode: requiredHostMode - ) - } else { self } - } - } - - public func addToLink(link: String) -> String { - "\(link)&data=\(encodeJSON(self).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)" - } - - public static func readFromLink(link: String) -> MigrationFileLinkData? { -// standaloneFileInfo(link) - nil - } -} - -public struct AppSettings: Codable, Equatable { - public var networkConfig: NetCfg? = nil - public var privacyEncryptLocalFiles: Bool? = nil - public var privacyAcceptImages: Bool? = nil - public var privacyLinkPreviews: Bool? = nil - public var privacyShowChatPreviews: Bool? = nil - public var privacySaveLastDraft: Bool? = nil - public var privacyProtectScreen: Bool? = nil - public var notificationMode: AppSettingsNotificationMode? = nil - public var notificationPreviewMode: NotificationPreviewMode? = nil - public var webrtcPolicyRelay: Bool? = nil - public var webrtcICEServers: [String]? = nil - public var confirmRemoteSessions: Bool? = nil - public var connectRemoteViaMulticast: Bool? = nil - public var connectRemoteViaMulticastAuto: Bool? = nil - public var developerTools: Bool? = nil - public var confirmDBUpgrades: Bool? = nil - public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil - public var iosCallKitEnabled: Bool? = nil - public var iosCallKitCallsInRecents: Bool? = nil - - public func prepareForExport() -> AppSettings { - var empty = AppSettings() - let def = AppSettings.defaults - if networkConfig != def.networkConfig { empty.networkConfig = networkConfig } - if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } - 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 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 } - return empty - } - - public static var defaults: AppSettings { - AppSettings ( - networkConfig: NetCfg.defaults, - privacyEncryptLocalFiles: true, - privacyAcceptImages: true, - privacyLinkPreviews: true, - privacyShowChatPreviews: true, - privacySaveLastDraft: true, - privacyProtectScreen: false, - 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 - ) - } -} - -public enum AppSettingsNotificationMode: String, Codable { - case off - case periodic - case instant - - public func toNotificationsMode() -> NotificationsMode { - switch self { - case .instant: .instant - case .periodic: .periodic - case .off: .off - } - } - - public static func from(_ mode: NotificationsMode) -> AppSettingsNotificationMode { - switch mode { - case .instant: .instant - case .periodic: .periodic - case .off: .off - } - } -} - -//public enum NotificationPreviewMode: Codable { -// case hidden -// case contact -// case message -//} - -public enum AppSettingsLockScreenCalls: String, Codable { - case disable - case show - case accept -} - -public struct UserNetworkInfo: Codable, Equatable { - public let networkType: UserNetworkType - public let online: Bool - - public init(networkType: UserNetworkType, online: Bool) { - self.networkType = networkType - self.online = online - } -} - -public enum UserNetworkType: String, Codable { - case none - case cellular - case wifi - case ethernet - case other - - public var text: LocalizedStringKey { - switch self { - case .none: "No network connection" - case .cellular: "Cellular" - case .wifi: "WiFi" - case .ethernet: "Wired ethernet" - case .other: "Other" - } +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 0511a8486c..29ccab7357 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -11,21 +11,36 @@ 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" @@ -42,6 +57,8 @@ public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphra 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" @@ -52,7 +69,10 @@ 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, @@ -66,12 +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 ]) } @@ -130,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 @@ -149,6 +181,12 @@ public let nseStateGroupDefault = EnumDefault( withDefault: .suspended // so that NSE that was never launched does not delay the app from resuming ) +public let seStateGroupDefault = EnumDefault( + 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 @@ -172,11 +210,21 @@ public let ntfPreviewModeGroupDefault = EnumDefault( 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(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( @@ -188,7 +236,25 @@ public let networkUseOnionHostsGroupDefault = EnumDefault( public let networkSessionModeGroupDefault = EnumDefault( defaults: groupDefaults, forKey: GROUP_DEFAULT_NETWORK_SESSION_MODE, - withDefault: .user + withDefault: .session +) + +public let networkSMPProxyModeGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE, + withDefault: .unknown +) + +public let networkSMPProxyFallbackGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK, + withDefault: .allowProtected +) + +public let networkSMPWebPortServersDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS, + withDefault: .preset ) public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE) @@ -245,12 +311,14 @@ public class EnumDefault where T.RawValue == String { } public class BoolDefault: Default { + @inline(__always) public func get() -> Bool { self.defaults.bool(forKey: self.key) } } public class IntDefault: Default { + @inline(__always) public func get() -> Int { self.defaults.integer(forKey: self.key) } @@ -260,11 +328,13 @@ public class Default { 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() @@ -272,9 +342,13 @@ public class Default { } 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) @@ -292,9 +366,13 @@ 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, @@ -306,9 +384,14 @@ 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) 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 24aca0dd18..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,13 +184,13 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile { Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences) } -public struct UserProfileUpdateSummary: Decodable { +public 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 = "*" @@ -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 public var fullDelete: ContactUserPreference public var reactions: ContactUserPreference @@ -483,7 +496,7 @@ public struct ContactUserPreferences: Decodable { ) } -public struct ContactUserPreference: Decodable { +public struct ContactUserPreference: Decodable, Hashable { public var enabled: FeatureEnabled public var userPreference: ContactUserPref

public var contactPreference: P @@ -495,7 +508,7 @@ public struct ContactUserPreference: 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: Decodable { +public enum ContactUserPref: Decodable, Hashable { case contact(preference: P) // contact override is set case user(preference: P) // global user default is used @@ -547,7 +560,7 @@ public protocol Feature { var text: String { get } } -public enum ChatFeature: String, Decodable, Feature { +public enum ChatFeature: String, Decodable, Feature, Hashable { case timedMessages case fullDelete case reactions @@ -690,7 +703,7 @@ 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 @@ -698,6 +711,7 @@ public enum GroupFeature: String, Decodable, Feature { case voice case files case simplexLinks + case reports case history public var id: Self { self } @@ -718,6 +732,7 @@ public enum GroupFeature: String, Decodable, Feature { case .voice: true case .files: true case .simplexLinks: true + case .reports: false case .history: false } } @@ -731,6 +746,7 @@ public enum GroupFeature: String, Decodable, 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") } } @@ -744,6 +760,7 @@ public enum GroupFeature: String, Decodable, Feature { case .voice: return "mic" case .files: return "doc" case .simplexLinks: return "link.circle" + case .reports: return "flag" case .history: return "clock" } } @@ -757,6 +774,7 @@ public enum GroupFeature: String, Decodable, Feature { 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" } } @@ -806,6 +824,11 @@ public enum GroupFeature: String, Decodable, Feature { 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." @@ -816,38 +839,43 @@ public enum GroupFeature: String, Decodable, Feature { 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. (24 hours)" - 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 "Group members can send SimpleX links." - case .off: return "SimpleX links are prohibited in this group." + 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 { @@ -890,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 @@ -968,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 @@ -986,7 +1014,7 @@ public enum FeatureAllowed: String, Codable, Identifiable { } } -public struct FullGroupPreferences: Decodable, Equatable { +public struct FullGroupPreferences: Decodable, Equatable, Hashable { public var timedMessages: TimedMessagesGroupPreference public var directMessages: RoleGroupPreference public var fullDelete: GroupPreference @@ -994,6 +1022,7 @@ public struct FullGroupPreferences: Decodable, Equatable { public var voice: RoleGroupPreference public var files: RoleGroupPreference public var simplexLinks: RoleGroupPreference + public var reports: GroupPreference public var history: GroupPreference public init( @@ -1004,6 +1033,7 @@ public struct FullGroupPreferences: Decodable, Equatable { voice: RoleGroupPreference, files: RoleGroupPreference, simplexLinks: RoleGroupPreference, + reports: GroupPreference, history: GroupPreference ) { self.timedMessages = timedMessages @@ -1013,6 +1043,7 @@ public struct FullGroupPreferences: Decodable, Equatable { self.voice = voice self.files = files self.simplexLinks = simplexLinks + self.reports = reports self.history = history } @@ -1024,11 +1055,12 @@ public struct FullGroupPreferences: Decodable, Equatable { 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: RoleGroupPreference? public var fullDelete: GroupPreference? @@ -1036,6 +1068,7 @@ public struct GroupPreferences: Codable { public var voice: RoleGroupPreference? public var files: RoleGroupPreference? public var simplexLinks: RoleGroupPreference? + public var reports: GroupPreference? public var history: GroupPreference? public init( @@ -1046,6 +1079,7 @@ public struct GroupPreferences: Codable { voice: RoleGroupPreference? = nil, files: RoleGroupPreference? = nil, simplexLinks: RoleGroupPreference? = nil, + reports: GroupPreference? = nil, history: GroupPreference? = nil ) { self.timedMessages = timedMessages @@ -1055,6 +1089,7 @@ public struct GroupPreferences: Codable { self.voice = voice self.files = files self.simplexLinks = simplexLinks + self.reports = reports self.history = history } @@ -1066,6 +1101,7 @@ public struct GroupPreferences: Codable { 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) ) } @@ -1079,11 +1115,12 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group voice: fullPreferences.voice, 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 { @@ -1107,7 +1144,7 @@ public struct GroupPreference: Codable, Equatable { } } -public struct RoleGroupPreference: Codable, Equatable { +public struct RoleGroupPreference: Codable, Equatable, Hashable { public var enable: GroupFeatureEnabled public var role: GroupMemberRole? @@ -1121,7 +1158,7 @@ public struct RoleGroupPreference: Codable, Equatable { } } -public struct TimedMessagesGroupPreference: Codable, Equatable { +public struct TimedMessagesGroupPreference: Codable, Equatable, Hashable { public var enable: GroupFeatureEnabled public var ttl: Int? @@ -1135,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 @@ -1150,21 +1187,21 @@ 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") @@ -1286,6 +1323,28 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } } + + 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 { @@ -1320,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 @@ -1370,7 +1436,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } - public enum ShowEnableVoiceMessagesAlert { + public enum ShowEnableVoiceMessagesAlert: Hashable { case userEnable case askContact case groupOwnerCan @@ -1398,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? { @@ -1409,6 +1504,22 @@ 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 { @@ -1442,8 +1553,26 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { 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 { + public struct SampleData: Hashable { public var direct: ChatInfo public var group: ChatInfo public var local: ChatInfo @@ -1460,14 +1589,20 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { ) } -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: [], @@ -1476,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 @@ -1504,15 +1645,26 @@ public struct Contact: Identifiable, Decodable, NamedChat { 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 } } @@ -1565,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 @@ -1583,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 @@ -1601,15 +1756,25 @@ public struct Connection: Decodable { 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, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled + 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 } @@ -1617,30 +1782,28 @@ public struct Connection: Decodable { static let sampleData = Connection( connId: 1, agentConnId: "abc", - peerChatVRange: VersionRange(minVersion: 1, maxVersion: 1), + peerChatVRange: VersionRange(1, 1), connStatus: .ready, connLevel: 0, viaGroupLink: false, pqSupport: false, - pqEncryption: 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 @@ -1650,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? @@ -1668,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 @@ -1680,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 } } @@ -1689,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, @@ -1697,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 @@ -1712,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) } @@ -1719,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") @@ -1787,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" @@ -1800,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 } @@ -1811,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] @@ -1821,28 +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 } @@ -1860,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 @@ -1894,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 @@ -1909,8 +2106,17 @@ public struct GroupMember: Identifiable, Decodable { 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 @@ -1951,14 +2157,26 @@ public struct GroupMember: Identifiable, Decodable { ? 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 @@ -1971,11 +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 @@ -1995,7 +2215,7 @@ public struct GroupMember: Identifiable, Decodable { 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 { @@ -2003,7 +2223,19 @@ public struct GroupMember: Identifiable, Decodable { 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 } @@ -2022,38 +2254,43 @@ public struct GroupMember: Identifiable, Decodable { 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, Codable { - 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") } @@ -2061,11 +2298,12 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod 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 } } @@ -2074,7 +2312,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod } } -public enum GroupMemberCategory: String, Decodable { +public enum GroupMemberCategory: String, Decodable, Hashable { case userMember = "user" case inviteeMember = "invitee" case hostMember = "host" @@ -2082,12 +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" @@ -2098,11 +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)" @@ -2115,11 +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" @@ -2131,7 +2375,7 @@ public enum GroupMemberStatus: String, Decodable { } } -public struct NoteFolder: Identifiable, Decodable, NamedChat { +public struct NoteFolder: Identifiable, Decodable, NamedChat, Hashable { public var noteFolderId: Int64 public var favorite: Bool public var unread: Bool @@ -2142,6 +2386,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat { 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 { "" } } @@ -2164,83 +2409,173 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat { ) } -public enum InvitedBy: Decodable { +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 - 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 @@ -2250,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? @@ -2258,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 } @@ -2426,13 +2762,16 @@ public struct ChatItem: Identifiable, Decodable { } } - public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? { + 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 } } @@ -2446,6 +2785,25 @@ public struct ChatItem: Identifiable, Decodable { 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( @@ -2527,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( @@ -2541,6 +2928,7 @@ public struct ChatItem: Identifiable, Decodable { itemDeleted: nil, itemEdited: false, itemLive: false, + userMention: false, deletable: false, editable: false ), @@ -2563,6 +2951,7 @@ public struct ChatItem: Identifiable, Decodable { itemDeleted: nil, itemEdited: false, itemLive: true, + userMention: false, deletable: false, editable: false ), @@ -2574,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, @@ -2585,7 +2974,7 @@ public struct ChatItem: Identifiable, Decodable { } } -public enum CIMergeCategory { +public enum CIMergeCategory: Hashable { case memberConnected case rcvGroupEvent case sndGroupEvent @@ -2594,7 +2983,7 @@ public enum CIMergeCategory { case chatFeature } -public enum CIDirection: Decodable { +public enum CIDirection: Decodable, Hashable { case directSnd case directRcv case groupSnd @@ -2614,13 +3003,21 @@ public enum CIDirection: Decodable { } } } + + 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? @@ -2628,10 +3025,11 @@ public struct CIMeta: Decodable { 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 } @@ -2641,10 +3039,6 @@ 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, deletable: Bool = true, editable: Bool = true) -> CIMeta { CIMeta( itemId: id, @@ -2656,6 +3050,7 @@ public struct CIMeta: Decodable { itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, + userMention: false, deletable: deletable, editable: editable ) @@ -2672,22 +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 { @@ -2705,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) } } @@ -2756,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 @@ -2766,19 +3195,126 @@ 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?) @@ -2794,12 +3330,12 @@ public enum CIDeleted: Decodable { } } -public enum MsgDirection: String, Decodable { +public enum MsgDirection: String, Decodable, Hashable { case rcv = "rcv" case snd = "snd" } -public enum CIForwardedFrom: Decodable { +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?) @@ -2812,6 +3348,20 @@ public enum CIForwardedFrom: Decodable { } } + 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)") @@ -2819,16 +3369,17 @@ public enum CIForwardedFrom: Decodable { } } -public enum CIDeleteMode: String, Decodable { +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 @@ -2859,7 +3410,7 @@ public enum CIContent: Decodable, ItemContent { case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo) case sndGroupE2EEInfo(e2eeInfo: E2EEInfo) case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo) - case invalidJSON(json: String) + case invalidJSON(json: Data?) public var text: String { get { @@ -2962,9 +3513,16 @@ public enum CIContent: Decodable, ItemContent { 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 @@ -2982,21 +3540,19 @@ 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" @@ -3020,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) @@ -3047,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 { @@ -3065,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) } @@ -3090,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 @@ -3116,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 } } @@ -3140,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? @@ -3199,22 +3796,28 @@ public struct CryptoFile: Codable { static var decryptedUrls = Dictionary() } -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( @@ -3242,51 +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) @@ -3298,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 } } @@ -3338,7 +3984,16 @@ public enum MsgContent: Equatable { } } - var cmdString: String { + public var isMediaOrFileAttachment: Bool { + switch self { + case .image: true + case .video: true + case .file: true + default: false + } + } + + public var cmdString: String { "json \(encodeJSON(self))" } @@ -3348,6 +4003,7 @@ public enum MsgContent: Equatable { case preview case image case duration + case reason } public static func == (lhs: MsgContent, rhs: MsgContent) -> Bool { @@ -3358,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 } @@ -3393,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") @@ -3430,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) @@ -3438,16 +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 @@ -3456,6 +4127,7 @@ 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 @@ -3469,21 +4141,23 @@ public enum Format: Decodable, Equatable { } } -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" @@ -3493,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 @@ -3525,31 +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 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 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 struct SndFileTransfer: Decodable { +public struct SndFileTransfer: Decodable, Hashable { } -public struct RcvFileTransfer: Decodable { +public struct RcvFileTransfer: Decodable, Hashable { public let fileId: Int64 } -public struct FileTransferMeta: Decodable { +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 { +public enum CICallStatus: String, Decodable, Hashable { case pending case missed case rejected @@ -3581,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 @@ -3598,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 @@ -3614,18 +4397,18 @@ public struct CIGroupInvitation: Decodable { } } -public enum CIGroupInvitationStatus: String, Decodable { +public enum CIGroupInvitationStatus: String, Decodable, Hashable { case pending case accepted case rejected case expired } -public struct E2EEInfo: Decodable { +public struct E2EEInfo: Decodable, Hashable { public var pqEnabled: Bool } -public enum RcvDirectEvent: Decodable { +public enum RcvDirectEvent: Decodable, Hashable { case contactDeleted case profileUpdated(fromProfile: Profile, toProfile: Profile) @@ -3654,7 +4437,7 @@ public enum RcvDirectEvent: Decodable { } } -public enum RcvGroupEvent: Decodable { +public enum RcvGroupEvent: Decodable, Hashable { case memberAdded(groupMemberId: Int64, profile: Profile) case memberConnected case memberLeft @@ -3710,7 +4493,7 @@ public enum RcvGroupEvent: Decodable { } } -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) @@ -3738,7 +4521,7 @@ public enum SndGroupEvent: Decodable { } } -public enum RcvConnEvent: Decodable { +public enum RcvConnEvent: Decodable, Hashable { case switchQueue(phase: SwitchPhase) case ratchetSync(syncStatus: RatchetSyncState) case verificationCodeReset @@ -3775,7 +4558,7 @@ 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) @@ -3812,56 +4595,64 @@ public enum SndConnEvent: Decodable { } } -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 { @@ -3869,13 +4660,64 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable { } } -public struct ChatItemInfo: Decodable { +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 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 { +public struct ChatItemVersion: Decodable, Hashable { public var chatItemVersionId: Int64 public var msgContent: MsgContent public var formattedText: [FormattedText]? @@ -3883,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(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( + 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( + _ errorAlert: Binding, + @ViewBuilder actions: (ErrorAlert) -> A = { _ in EmptyView() } + ) -> some View { + alert( + errorAlert.wrappedValue?.title ?? "", + isPresented: Binding( + 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 125600f3f3..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() @@ -40,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)! } @@ -85,6 +86,8 @@ public func deleteAppDatabaseAndFiles() { 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) @@ -196,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 { diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift similarity index 56% rename from apps/ios/Shared/Model/ImageUtils.swift rename to apps/ios/SimpleXChat/ImageUtils.swift index 6437597b19..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,35 @@ func imageHasAlpha(_ img: UIImage) -> Bool { return false } -func saveFileFromURL(_ url: URL) -> 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() { @@ -184,7 +221,7 @@ func saveFileFromURL(_ url: URL) -> CryptoFile? { return savedFile } -func moveTempFileFromURL(_ url: URL) -> CryptoFile? { +public func moveTempFileFromURL(_ url: URL) -> CryptoFile? { do { let encrypted = privacyEncryptLocalFilesGroupDefault.get() let fileName = uniqueCombine(url.lastPathComponent) @@ -197,7 +234,6 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? { try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName)) savedFile = CryptoFile.plain(fileName) } - ChatModel.shared.filesToDelete.remove(url) return savedFile } catch { logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)") @@ -205,7 +241,53 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? { } } -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) } @@ -237,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,") } @@ -245,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) @@ -258,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) @@ -304,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 = { + var cache = NSCache() + 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> +public typealias SESubscriber = SharedFileSubscriber> + public class SharedFileSubscriber: 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: 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) 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) 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 153365424e..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[]); 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 adacd57310..e75173d6cf 100644 --- a/apps/ios/SimpleXChat/hs_init.c +++ b/apps/ios/SimpleXChat/hs_init.c @@ -25,6 +25,22 @@ void haskell_init(void) { } 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", diff --git a/apps/ios/SimpleXChat/hs_init.h b/apps/ios/SimpleXChat/hs_init.h index a732fd7113..40be4fc263 100644 --- a/apps/ios/SimpleXChat/hs_init.h +++ b/apps/ios/SimpleXChat/hs_init.h @@ -13,4 +13,6 @@ 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 + +@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 06d9f1f43e..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)" = " (може да се копира)"; @@ -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,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Звезда в GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Добави контакт**: за създаване на нов линк или свързване чрез получен линк за връзка."; - -/* 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 group**: to create a new group." = "**Създай група**: за създаване на нова група."; @@ -80,10 +47,10 @@ "**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. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Моля, обърнете внимание**: използването на една и съща база данни на две устройства ще наруши декриптирането на съобщенията от вашите връзки като защита на сигурността."; @@ -92,7 +59,7 @@ "**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. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: Незабавните push известия изискват парола, запазена в Keychain."; @@ -154,15 +121,15 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ е потвърдено"; -/* 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 членове"; @@ -229,9 +196,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 секунди"; @@ -277,7 +241,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 ден"; /* time interval */ @@ -286,12 +251,17 @@ /* 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" = "Еднократен линк"; + /* No comment provided by engineer. */ "5 minutes" = "5 минути"; @@ -326,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"; @@ -338,54 +305,95 @@ "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 contact" = "Добави контакт"; - -/* 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" = "админ"; @@ -401,6 +409,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Разширени мрежови настройки"; +/* No comment provided by engineer. */ +"Advanced settings" = "Разширени настройки"; + /* chat item text */ "agreeing encryption for %@…" = "съгласуване на криптиране за %@…"; @@ -416,6 +427,9 @@ /* 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." = "Всички членове на групата ще останат свързани."; @@ -431,6 +445,9 @@ /* 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." = "Всички ваши контакти ще останат свързани."; @@ -446,9 +463,15 @@ /* 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 downgrade" = "Позволи понижаване"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа)"; @@ -464,6 +487,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Разреши изпращането на изчезващи съобщения."; +/* No comment provided by engineer. */ +"Allow sharing" = "Позволи споделяне"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Позволи необратимо изтриване на изпратените съобщения. (24 часа)"; @@ -509,6 +535,9 @@ /* pref value */ "always" = "винаги"; +/* No comment provided by engineer. */ +"Always use private routing." = "Винаги използвай поверително рутиране."; + /* No comment provided by engineer. */ "Always use relay" = "Винаги използвай реле"; @@ -521,6 +550,9 @@ /* No comment provided by engineer. */ "Answer call" = "Отговор на повикване"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; + /* No comment provided by engineer. */ "App build: %@" = "Компилация на приложението: %@"; @@ -539,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" = "Версия на приложението"; @@ -551,9 +586,18 @@ /* 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" = "Архивиране на база данни"; @@ -599,9 +643,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" = "Грешен адрес на настолното устройство"; @@ -617,12 +667,33 @@ /* 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" = "Блокирай"; @@ -647,12 +718,19 @@ /* rcv group event chat item */ "blocked %@" = "блокиран %@"; -/* marked deleted chat item preview text */ +/* 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" = "удебелен"; @@ -674,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) (БЕТА)."; @@ -692,16 +776,26 @@ /* No comment provided by engineer. */ "Calls" = "Обаждания"; +/* No comment provided by engineer. */ +"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!" = "Не може да покани контакта!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Не може да поканят контактите!"; -/* No comment provided by engineer. */ +/* alert action +alert button */ "Cancel" = "Отказ"; /* No comment provided by engineer. */ @@ -713,15 +807,21 @@ /* 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?" = "Промяна на паролата на базата данни?"; @@ -747,7 +847,7 @@ "Change self-destruct mode" = "Промени режима на самоунищожение"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Промени кода за достъп за самоунищожение"; /* chat item text */ @@ -765,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" = "Чатът работи"; @@ -796,9 +893,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. */ @@ -813,7 +913,7 @@ /* No comment provided by engineer. */ "Choose from library" = "Избери от библиотеката"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "Изчисти"; /* No comment provided by engineer. */ @@ -831,9 +931,6 @@ /* No comment provided by engineer. */ "colored" = "цветен"; -/* No comment provided by engineer. */ -"Colors" = "Цветове"; - /* server test step */ "Compare file" = "Сравни файл"; @@ -945,7 +1042,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Свързване с настолно устройство"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "свързване…"; /* No comment provided by engineer. */ @@ -993,9 +1090,6 @@ /* notification */ "Contact is connected" = "Контактът е свързан"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "Контактът все още не е свързан!"; - /* No comment provided by engineer. */ "Contact name" = "Име на контакт"; @@ -1011,7 +1105,7 @@ /* No comment provided by engineer. */ "Continue" = "Продължи"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "Копирай"; /* No comment provided by engineer. */ @@ -1021,19 +1115,19 @@ "Correct name to %@?" = "Поправи име на %@?"; /* No comment provided by engineer. */ -"Create" = "Създай"; +"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" = "Създай файл"; +"Create file" = "Създаване на файл"; /* No comment provided by engineer. */ -"Create group" = "Създай група"; +"Create group" = "Създаване на група"; /* No comment provided by engineer. */ "Create group link" = "Създай групов линк"; @@ -1054,7 +1148,7 @@ "Create secret group" = "Създай тайна група"; /* No comment provided by engineer. */ -"Create SimpleX address" = "Създай SimpleX адрес"; +"Create SimpleX address" = "Създаване на адрес в SimpleX"; /* No comment provided by engineer. */ "Create your profile" = "Създай своя профил"; @@ -1065,9 +1159,6 @@ /* copied message info */ "Created at: %@" = "Създаден на: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Създаден на %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Създаване на архивен линк"; @@ -1161,7 +1252,8 @@ /* message decrypt error item */ "Decryption error" = "Грешка при декриптиране"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "по подразбиране (%@)"; /* No comment provided by engineer. */ @@ -1170,7 +1262,8 @@ /* No comment provided by engineer. */ "default (yes)" = "по подразбиране (да)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Изтрий"; /* No comment provided by engineer. */ @@ -1191,12 +1284,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Изтрий и уведоми контакт"; -/* No comment provided by engineer. */ -"Delete archive" = "Изтрий архив"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Изтриване на архива на чата?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Изтрий чат профила"; @@ -1209,12 +1296,6 @@ /* No comment provided by engineer. */ "Delete contact" = "Изтрий контакт"; -/* No comment provided by engineer. */ -"Delete Contact" = "Изтрий контакт"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Изтрий контакт?\nТова не може да бъде отменено!"; - /* No comment provided by engineer. */ "Delete database" = "Изтрий базата данни"; @@ -1257,7 +1338,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Изтрий съобщението?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Изтрий съобщенията"; /* No comment provided by engineer. */ @@ -1269,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?" = "Изтрий предстоящата връзка?"; @@ -1297,7 +1375,7 @@ "deleted contact" = "изтрит контакт"; /* rcv group event chat item */ -"deleted group" = "групата изтрита"; +"deleted group" = "групата е изтрита"; /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1348,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)" = "Деактивиране (запазване на промените)"; @@ -1372,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" = "Изчезва в"; @@ -1413,7 +1491,8 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Понижи версията и отвори чата"; -/* chat item action */ +/* alert button +chat item action */ "Download" = "Изтегли"; /* No comment provided by engineer. */ @@ -1452,7 +1531,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. */ @@ -1617,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)" = "Грешка при добавяне на член(ове)"; @@ -1654,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!" = "Грешка при изтриването на чата!"; @@ -1662,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" = "Грешка при изтриване на базата данни"; @@ -1690,29 +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" = "Грешка при зареждане на %@ сървъри"; - /* No comment provided by engineer. */ "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" = "Грешка при запазване на профила на групата"; @@ -1752,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. */ @@ -1779,7 +1846,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. */ @@ -1791,9 +1860,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" = "Изход без запазване"; @@ -1824,7 +1890,7 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "По-бързо присъединяване и по-надеждни съобщения."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Любим"; /* No comment provided by engineer. */ @@ -1846,7 +1912,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 not allowed" = "Файлове и медия не са разрешени"; @@ -1917,9 +1983,6 @@ /* 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." = "Напълно децентрализирана – видима е само за членовете."; @@ -1968,27 +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. (24 hours)" = "Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)"; - -/* 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 SimpleX links." = "Членовете на групата могат да изпращат SimpleX линкове."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; - /* notification */ "Group message:" = "Групово съобщение:"; @@ -2049,9 +2091,6 @@ /* time unit */ "hours" = "часове"; -/* No comment provided by engineer. */ -"How it works" = "Как работи"; - /* No comment provided by engineer. */ "How SimpleX works" = "Как работи SimpleX"; @@ -2095,13 +2134,13 @@ "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" = "Импортиране на база данни"; @@ -2185,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" = "Интерфейс"; @@ -2223,7 +2262,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Невалиден отговор"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Невалиден адрес на сървъра!"; /* item status text */ @@ -2269,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." = "Позволява да имате много анонимни връзки без споделени данни между тях в един чат профил ."; @@ -2292,7 +2331,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японски интерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Присъединяване"; /* No comment provided by engineer. */ @@ -2319,13 +2358,13 @@ /* No comment provided by engineer. */ "Joining group" = "Присъединяване към групата"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Запази"; /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Дръжте приложението отворено, за да го използвате от настолното устройство"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Запази неизползваната покана за връзка?"; /* No comment provided by engineer. */ @@ -2343,7 +2382,7 @@ /* No comment provided by engineer. */ "Learn more" = "Научете повече"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Напусни"; /* No comment provided by engineer. */ @@ -2382,9 +2421,6 @@ /* No comment provided by engineer. */ "Live messages" = "Съобщения на живо"; -/* No comment provided by engineer. */ -"Local" = "Локално"; - /* No comment provided by engineer. */ "Local name" = "Локално име"; @@ -2397,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" = "Маркирай като изтрито за всички"; @@ -2454,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" = "Грешка при доставката на съобщението"; @@ -2470,7 +2518,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" = "получено съобщение"; @@ -2533,7 +2581,7 @@ "Migration is completed" = "Миграцията е завършена"; /* No comment provided by engineer. */ -"Migrations: %@" = "Миграции: %@"; +"Migrations:" = "Миграции:"; /* time unit */ "minutes" = "минути"; @@ -2568,19 +2616,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. */ @@ -2598,7 +2643,7 @@ /* No comment provided by engineer. */ "Network status" = "Състояние на мрежата"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "никога"; /* No comment provided by engineer. */ @@ -2610,9 +2655,6 @@ /* notification */ "New contact:" = "Нов контакт:"; -/* No comment provided by engineer. */ -"New database archive" = "Нов архив на база данни"; - /* No comment provided by engineer. */ "New desktop app!" = "Ново настолно приложение!"; @@ -2676,12 +2718,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. */ "Not compatible!" = "Несъвместим!"; @@ -2698,11 +2746,11 @@ "observer" = "наблюдател"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "изключено"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Изключено"; /* feature offered item */ @@ -2711,7 +2759,7 @@ /* feature offered item */ "offered %@: %@" = "предлага %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ок"; /* No comment provided by engineer. */ @@ -2720,9 +2768,6 @@ /* No comment provided by engineer. */ "Old database" = "Стара база данни"; -/* No comment provided by engineer. */ -"Old database archive" = "Стар архив на база данни"; - /* group pref value */ "on" = "включено"; @@ -2730,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." = "Само собствениците на групата могат да променят груповите настройки."; @@ -2780,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. */ @@ -2798,12 +2843,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 app…" = "Приложението се отваря…"; @@ -2865,10 +2904,7 @@ "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" = "Постоянна грешка при декриптиране"; @@ -2933,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" = "Предварително зададен адрес на сървъра"; @@ -2963,16 +2996,10 @@ /* No comment provided by engineer. */ "Profile images" = "Профилни изображения"; -/* No comment provided by engineer. */ -"Profile name" = "Име на профила"; - -/* No comment provided by engineer. */ -"Profile name:" = "Име на профила:"; - /* 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. */ @@ -3032,17 +3059,17 @@ /* 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/chat-profiles.html#incognito-mode)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"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)."; @@ -3050,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то за доставка е деактивирано"; @@ -3077,9 +3101,6 @@ /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн."; -/* No comment provided by engineer. */ -"Receiving concurrency" = "Паралелност на получаване"; - /* No comment provided by engineer. */ "Receiving file will be stopped." = "Получаващият се файл ще бъде спрян."; @@ -3110,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. */ @@ -3218,9 +3240,6 @@ /* chat item action */ "Reveal" = "Покажи"; -/* No comment provided by engineer. */ -"Revert" = "Отмени промените"; - /* No comment provided by engineer. */ "Revoke" = "Отзови"; @@ -3239,13 +3258,14 @@ /* No comment provided by engineer. */ "Safer groups" = "По-безопасни групи"; -/* 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. */ @@ -3254,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" = "Запази профила на групата"; @@ -3269,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. */ @@ -3278,12 +3292,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?" = "Запази съобщението при посрещане?"; @@ -3350,7 +3361,7 @@ /* chat item text */ "security code changed" = "кодът за сигурност е променен"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Избери"; /* No comment provided by engineer. */ @@ -3377,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" = "Изпрати лично съобщение за свързване"; @@ -3395,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" = "Изпращайте въпроси и идеи"; @@ -3410,7 +3415,7 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Изпращане до последните 100 съобщения на нови членове."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Подателят отмени прехвърлянето на файла."; /* No comment provided by engineer. */ @@ -3509,7 +3514,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Променете формата на профилните изображения"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "Сподели"; /* No comment provided by engineer. */ @@ -3518,7 +3524,7 @@ /* No comment provided by engineer. */ "Share address" = "Сподели адрес"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Сподели адреса с контактите?"; /* No comment provided by engineer. */ @@ -3570,7 +3576,7 @@ "SimpleX links" = "SimpleX линкове"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "SimpleX линкове са забранени в тази група."; +"SimpleX links are prohibited." = "SimpleX линкове са забранени в тази група."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "SimpleX линковете не са разрешени"; @@ -3602,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." = "Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности."; @@ -3635,9 +3638,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Спри чата"; -/* 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." = "Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян."; @@ -3653,10 +3653,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 */ @@ -3704,9 +3704,6 @@ /* No comment provided by engineer. */ "Tap to scan" = "Докосни за сканиране"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "Докосни за започване на нов чат"; - /* No comment provided by engineer. */ "TCP connection timeout" = "Времето на изчакване за установяване на TCP връзка"; @@ -3728,7 +3725,7 @@ /* No comment provided by engineer. */ "Test servers" = "Тествай сървърите"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Тестовете са неуспешни!"; /* No comment provided by engineer. */ @@ -3740,9 +3737,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." = "Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате."; @@ -3764,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." = "Хешът на предишното съобщение е различен."; @@ -3776,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! ✅" = "Втората отметка, която пропуснахме! ✅"; @@ -3797,9 +3791,6 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Текстът, който поставихте, не е SimpleX линк за връзка."; -/* No comment provided by engineer. */ -"Theme" = "Тема"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Тези настройки са за текущия ви профил **%@**."; @@ -3857,15 +3848,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 има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; - /* 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 има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон."; @@ -3920,13 +3911,10 @@ /* rcv group event chat item */ "unblocked %@" = "отблокиран %@"; -/* 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. */ @@ -3974,10 +3962,10 @@ /* 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. */ @@ -3986,18 +3974,12 @@ /* 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" = "актуализиран профил на групата"; @@ -4007,9 +3989,6 @@ /* 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" = "Актуализирай и отвори чата"; @@ -4055,12 +4034,6 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Използвайте приложението по време на разговора."; -/* 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."; @@ -4140,7 +4113,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 not allowed" = "Гласовите съобщения не са разрешени"; @@ -4199,9 +4172,6 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "При свързване на аудио и видео разговори."; -/* 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." = "Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани."; @@ -4229,18 +4199,12 @@ /* 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" = "Вие"; - /* No comment provided by engineer. */ "You **must not** use the same database on two devices." = "**Не трябва** да използвате една и съща база данни на две устройства."; @@ -4314,7 +4278,7 @@ "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." = "Можете да зададете визуализация на известията на заключен екран през настройките."; @@ -4325,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" = "Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението"; @@ -4337,7 +4298,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Можете да използвате markdown за форматиране на съобщенията:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Можете да видите отново линкът за покана в подробностите за връзката."; /* No comment provided by engineer. */ @@ -4356,10 +4317,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!" = "Вече сте заявили връзка през този адрес!"; @@ -4367,9 +4328,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." = "Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството."; @@ -4445,24 +4403,18 @@ /* 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 (%@)." = "Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@)."; @@ -4473,7 +4425,7 @@ "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" = "Вашият текущ профил"; @@ -4494,7 +4446,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." = "Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство."; @@ -4502,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" = "Вашият адрес на сървъра"; @@ -4512,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/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index e2087547bd..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)"; @@ -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,9 +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. */ "**e2e encrypted** audio call" = "**e2e šifrovaný** audio hovor"; @@ -65,16 +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)."; +"**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."; +"**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."; @@ -121,6 +91,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ připojen"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ staženo"; + /* notification title */ "%@ is connected!" = "%@ je připojen!"; @@ -130,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"; @@ -145,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"; @@ -178,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"; @@ -229,7 +226,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 den"; /* time interval */ @@ -238,10 +236,12 @@ /* 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. */ @@ -277,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. */ @@ -302,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 */ @@ -311,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ů."; @@ -440,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: %@"; @@ -566,7 +559,8 @@ /* 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 */ @@ -575,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. */ @@ -606,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 */ @@ -624,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"; @@ -649,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. */ @@ -663,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. */ @@ -678,9 +672,6 @@ /* No comment provided by engineer. */ "colored" = "barevné"; -/* No comment provided by engineer. */ -"Colors" = "Barvy"; - /* server test step */ "Compare file" = "Porovnat soubor"; @@ -753,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. */ @@ -795,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"; @@ -813,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. */ @@ -822,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"; @@ -837,6 +822,9 @@ /* No comment provided by engineer. */ "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 profile" = "Vytvořte si profil"; + /* server test step */ "Create queue" = "Vytvořit frontu"; @@ -849,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"; @@ -939,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. */ @@ -948,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. */ @@ -963,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"; @@ -981,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"; @@ -1023,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. */ @@ -1035,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í?"; @@ -1102,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í)"; @@ -1126,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"; @@ -1185,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. */ @@ -1320,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(ů)"; @@ -1362,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"; @@ -1395,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"; @@ -1440,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. */ @@ -1461,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. */ @@ -1494,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. */ @@ -1516,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!"; @@ -1560,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í!"; @@ -1602,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. (24 hours)" = "Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)"; - -/* 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:"; @@ -1677,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"; @@ -1720,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"; @@ -1789,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í"; @@ -1809,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 */ @@ -1855,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."; @@ -1878,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. */ @@ -1908,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. */ @@ -1938,9 +1879,6 @@ /* No comment provided by engineer. */ "Live messages" = "Živé zprávy"; -/* No comment provided by engineer. */ -"Local" = "Místní"; - /* No comment provided by engineer. */ "Local name" = "Místní název"; @@ -1953,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"; @@ -2007,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"; @@ -2023,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"; @@ -2050,7 +1997,7 @@ "Migration is completed" = "Přenesení dokončeno"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrace: %@"; +"Migrations:" = "Migrace:"; /* time unit */ "minutes" = "minut"; @@ -2082,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. */ @@ -2106,7 +2050,7 @@ /* No comment provided by engineer. */ "Network status" = "Stav sítě"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "nikdy"; /* notification */ @@ -2115,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!"; @@ -2178,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í"; @@ -2197,11 +2144,11 @@ "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"; /* feature offered item */ @@ -2210,15 +2157,12 @@ /* 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"; @@ -2226,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."; @@ -2276,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. */ @@ -2288,12 +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."; - /* member role */ "owner" = "vlastník"; @@ -2322,10 +2260,7 @@ "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í"; @@ -2381,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"; @@ -2408,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. */ @@ -2456,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)."; @@ -2471,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"; @@ -2522,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. */ @@ -2606,9 +2536,6 @@ /* chat item action */ "Reveal" = "Odhalit"; -/* No comment provided by engineer. */ -"Revert" = "Vrátit"; - /* No comment provided by engineer. */ "Revoke" = "Odvolat"; @@ -2624,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. */ @@ -2639,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"; @@ -2654,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. */ @@ -2663,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?"; @@ -2711,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. */ @@ -2738,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í"; @@ -2756,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ů"; @@ -2768,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. */ @@ -2852,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. */ @@ -2861,7 +2775,7 @@ /* 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. */ @@ -2933,9 +2847,6 @@ /* 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."; @@ -2954,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."; @@ -2972,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 */ @@ -3011,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"; @@ -3035,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. */ @@ -3047,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í."; @@ -3068,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ší."; @@ -3080,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! ✅"; @@ -3098,9 +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. */ "These settings are for your current profile **%@**." = "Toto nastavení je pro váš aktuální profil **%@**."; @@ -3137,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."; @@ -3179,13 +3078,10 @@ /* 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. */ @@ -3224,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"; @@ -3284,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."; @@ -3345,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!"; @@ -3389,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."; @@ -3404,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í"; @@ -3453,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í."; @@ -3464,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"; @@ -3491,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í."; @@ -3566,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"; @@ -3581,9 +3444,6 @@ /* 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 (%@)."; @@ -3612,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í."; @@ -3620,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"; @@ -3632,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 5fc393fa1f..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)"; @@ -31,30 +16,15 @@ /* 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. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* 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,7 @@ "[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 contact**: to create a new invitation link, or connect via a link you received." = "**Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen oder eine Verbindung über einen Link herzustellen, den Sie erhalten haben."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Neuen Kontakt hinzufügen**: Um einen Einmal-QR-Code oder -Link für Ihren Kontakt zu erzeugen."; +"**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 group**: to create a new group." = "**Gruppe erstellen**: Um eine neue Gruppe zu erstellen."; @@ -80,10 +47,10 @@ "**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. */ "**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."; @@ -92,7 +59,10 @@ "**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 / 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."; @@ -155,7 +125,10 @@ "%@ 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"; @@ -163,6 +136,9 @@ /* 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"; @@ -175,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"; @@ -187,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)"; @@ -229,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"; @@ -277,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"; @@ -286,11 +278,22 @@ /* 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"; @@ -317,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 Empfängeradresse abbrechen"; +"Abort changing address" = "Wechsel der Empfängeradresse beenden"; /* No comment provided by engineer. */ -"Abort changing address?" = "Wechsel der Empfängeradresse 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"; @@ -338,53 +338,106 @@ "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. */ +"Accepted conditions" = "Akzeptierte Nutzungsbedingungen"; + +/* chat list item title */ +"accepted invitation" = "Einladung angenommen"; + +/* No comment provided by engineer. */ +"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 contact" = "Kontakt hinzufügen"; +"Add friends" = "Freunde aufnehmen"; /* No comment provided by engineer. */ -"Add preset servers" = "Füge voreingestellte Server hinzu"; +"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 Empfängeradresse 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"; @@ -401,21 +454,33 @@ /* 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."; @@ -423,14 +488,26 @@ "all members" = "Alle Mitglieder"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!"; +"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! 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."; +"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."; @@ -438,7 +515,7 @@ "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-Server hochgeladen."; +"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"; @@ -446,9 +523,15 @@ /* No comment provided by engineer. */ "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 calls?" = "Anrufe erlauben?"; + /* No comment provided by engineer. */ "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)"; @@ -464,9 +547,15 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Das Senden von verschwindenden Nachrichten erlauben."; +/* No comment provided by engineer. */ +"Allow sharing" = "Teilen erlauben"; + /* No comment provided by engineer. */ "Allow 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."; @@ -509,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"; @@ -518,9 +610,15 @@ /* 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: %@"; @@ -530,6 +628,9 @@ /* 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"; @@ -539,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"; @@ -551,15 +655,48 @@ /* 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"; @@ -599,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"; @@ -617,12 +760,39 @@ /* 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"; @@ -647,12 +817,19 @@ /* rcv group event chat item */ "blocked %@" = "%@ wurde blockiert"; -/* marked deleted chat item preview text */ +/* 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"; @@ -674,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!"; @@ -692,9 +884,18 @@ /* No comment provided by engineer. */ "Calls" = "Anrufe"; +/* No comment provided by engineer. */ +"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!"; @@ -702,6 +903,10 @@ "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. */ @@ -714,14 +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" = "Zellulär"; +"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?"; @@ -747,7 +964,7 @@ "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 */ @@ -766,7 +983,16 @@ "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"; @@ -777,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"; @@ -789,16 +1018,40 @@ /* 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. */ @@ -814,25 +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 private notes?" = "Private Notizen löschen?"; +"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"; @@ -843,15 +1117,48 @@ /* 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"; @@ -870,6 +1177,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Hochladen bestätigen"; +/* token status text */ +"Confirmed" = "Bestätigt"; + /* server test step */ "Connect" = "Verbinden"; @@ -885,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?"; @@ -909,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)"; @@ -942,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"; @@ -960,18 +1291,39 @@ /* 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"; @@ -981,6 +1333,9 @@ /* 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"; @@ -994,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"; @@ -1002,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 %@?"; @@ -1024,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"; @@ -1041,6 +1411,9 @@ /* No comment provided by engineer. */ "Create link" = "Link erzeugen"; +/* No comment provided by engineer. */ +"Create list" = "Liste erstellen"; + /* 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/). 💻"; @@ -1059,15 +1432,15 @@ /* No comment provided by engineer. */ "Create your profile" = "Erstellen Sie Ihr Profil"; +/* No comment provided by engineer. */ +"Created" = "Erstellt"; + /* No comment provided by engineer. */ "Created at" = "Erstellt um"; /* copied message info */ "Created at: %@" = "Erstellt um: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Erstellt am %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Archiv-Link erzeugen"; @@ -1077,12 +1450,18 @@ /* 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 %@."; @@ -1092,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"; @@ -1155,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)"; @@ -1170,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?"; @@ -1192,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"; @@ -1203,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"; @@ -1210,10 +1612,7 @@ "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"; @@ -1251,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"; @@ -1270,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"; @@ -1281,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"; @@ -1299,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"; @@ -1320,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"; @@ -1348,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"; @@ -1362,6 +1809,9 @@ /* No comment provided by engineer. */ "disabled" = "deaktiviert"; +/* No comment provided by engineer. */ +"Disabled" = "Deaktiviert"; + /* No comment provided by engineer. */ "Disappearing message" = "Verschwindende Nachricht"; @@ -1372,7 +1822,7 @@ "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"; @@ -1399,7 +1849,19 @@ "Do not send history to new members." = "Den Nachrichtenverlauf nicht an neue Mitglieder senden."; /* No comment provided by engineer. */ -"Do NOT use SimpleX for emergency calls." = "Nutzen Sie SimpleX nicht für Notrufe."; +"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"; @@ -1407,21 +1869,40 @@ /* 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"; -/* chat item action */ +/* 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"; @@ -1434,12 +1915,18 @@ /* 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"; @@ -1452,12 +1939,15 @@ /* 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"; @@ -1491,6 +1981,9 @@ /* enabled status */ "enabled" = "Aktiviert"; +/* No comment provided by engineer. */ +"Enabled" = "Aktiviert"; + /* No comment provided by engineer. */ "Enabled for" = "Aktiviert für"; @@ -1566,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"; @@ -1612,26 +2108,41 @@ "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 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"; /* 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"; @@ -1641,6 +2152,9 @@ /* 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"; @@ -1650,6 +2164,9 @@ /* 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"; @@ -1662,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"; @@ -1692,26 +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"; +/* alert title */ +"Error loading servers" = "Fehler beim Laden der Server"; + /* No comment provided by engineer. */ -"Error loading %@ servers" = "Fehler beim Laden von %@ Servern"; +"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 receiving file" = "Fehler beim Empfangen der Datei"; +"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"; @@ -1725,6 +2260,9 @@ /* 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"; @@ -1753,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"; @@ -1779,7 +2326,9 @@ /* No comment provided by engineer. */ "Error: " = "Fehler: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Fehler: %@"; /* No comment provided by engineer. */ @@ -1789,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"; @@ -1800,12 +2352,21 @@ /* 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."; @@ -1821,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"; @@ -1846,7 +2440,7 @@ "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"; @@ -1887,15 +2481,45 @@ /* 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"; @@ -1905,6 +2529,24 @@ /* 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"; @@ -1917,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."; @@ -1929,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"; @@ -1968,27 +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. (24 hours)" = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; - -/* 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 SimpleX links." = "Gruppenmitglieder können SimpleX-Links senden."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden."; - /* notification */ "Group message:" = "Grppennachricht:"; @@ -2011,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"; @@ -2050,6 +2683,12 @@ "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. */ @@ -2086,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"; @@ -2109,9 +2748,15 @@ /* 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"; @@ -2130,6 +2775,15 @@ /* 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"; @@ -2161,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"; @@ -2184,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"; @@ -2223,7 +2895,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Ungültige Reaktion"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Ungültige Serveradresse!"; /* item status text */ @@ -2235,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"; @@ -2262,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"; @@ -2269,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."; @@ -2280,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 (%@)."; @@ -2292,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. */ @@ -2319,13 +3003,16 @@ /* No comment provided by engineer. */ "Joining group" = "Der Gruppe beitreten"; -/* No comment provided by engineer. */ +/* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Nicht genutzte Einladung behalten?"; /* No comment provided by engineer. */ @@ -2343,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"; @@ -2373,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"; @@ -2382,9 +3084,6 @@ /* No comment provided by engineer. */ "Live messages" = "Live Nachrichten"; -/* No comment provided by engineer. */ -"Local" = "Lokal"; - /* No comment provided by engineer. */ "Local name" = "Lokaler Name"; @@ -2397,24 +3096,15 @@ /* 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"; /* No comment provided by engineer. */ "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 getrennt 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?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Für Alle als gelöscht markieren"; @@ -2433,6 +3123,12 @@ /* 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"; @@ -2445,6 +3141,15 @@ /* 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."; @@ -2452,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"; @@ -2460,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"; @@ -2470,14 +3223,29 @@ "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"; @@ -2493,11 +3261,23 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Die Nachrichten von %@ werden angezeigt!"; -/* 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, Ablehnung und Einbruchs-Wiederherstellung geschützt."; +/* alert message */ +"Messages in this chat will never be deleted." = "Nachrichten in diesem Chat werden nie gelöscht."; /* 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, Ablehnung und Einbruchs-Wiederherstellung geschützt."; +"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"; @@ -2533,7 +3313,7 @@ "Migration is completed" = "Die Migration wurde abgeschlossen"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrationen: %@"; +"Migrations:" = "Migrationen:"; /* time unit */ "minutes" = "Minuten"; @@ -2556,31 +3336,40 @@ /* 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. */ @@ -2589,39 +3378,57 @@ /* 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"; @@ -2637,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"; @@ -2646,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"; @@ -2658,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"; @@ -2670,27 +3498,87 @@ /* No comment provided by engineer. */ "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)"; @@ -2698,11 +3586,11 @@ "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"; /* feature offered item */ @@ -2711,7 +3599,7 @@ /* feature offered item */ "offered %@: %@" = "angeboten %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2720,9 +3608,6 @@ /* No comment provided by engineer. */ "Old database" = "Alte Datenbank"; -/* No comment provided by engineer. */ -"Old database archive" = "Altes Datenbankarchiv"; - /* group pref value */ "on" = "Ein"; @@ -2730,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."; @@ -2750,6 +3641,12 @@ /* 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."; @@ -2780,15 +3677,21 @@ /* 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"; @@ -2798,15 +3701,18 @@ /* No comment provided by engineer. */ "Open Settings" = "Geräte-Einstellungen öffnen"; -/* authentication reason */ -"Open user profiles" = "Benutzerprofile öffnen"; - -/* 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."; - /* No comment provided by engineer. */ "Opening app…" = "App wird geöffnet…"; +/* No comment provided by engineer. */ +"Operator" = "Betreiber"; + +/* alert title */ +"Operator server" = "Betreiber-Server"; + +/* No comment provided by engineer. */ +"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"; @@ -2819,9 +3725,24 @@ /* 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"; @@ -2843,6 +3764,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "Zugangscode eingestellt!"; +/* No comment provided by engineer. */ +"Password" = "Passwort"; + /* No comment provided by engineer. */ "Password to show" = "Passwort anzeigen"; @@ -2865,10 +3789,16 @@ "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"; @@ -2882,9 +3812,18 @@ /* 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."; @@ -2924,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"; @@ -2934,26 +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"; @@ -2963,16 +3941,13 @@ /* No comment provided by engineer. */ "Profile images" = "Profil-Bilder"; -/* No comment provided by engineer. */ -"Profile name" = "Profilname"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profilname:"; - /* 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. */ @@ -2987,6 +3962,9 @@ /* 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."; @@ -3005,15 +3983,30 @@ /* 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"; @@ -3029,20 +4022,23 @@ /* 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/chat-profiles.html#incognito-mode)." = "Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](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."; @@ -3051,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…"; @@ -3074,14 +4070,20 @@ /* 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 concurrency" = "Gleichzeitiger Empfang"; - -/* 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"; @@ -3095,9 +4097,24 @@ /* 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?"; @@ -3110,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. */ @@ -3119,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"; @@ -3131,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"; @@ -3182,18 +4218,66 @@ /* 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"; @@ -3219,7 +4303,7 @@ "Reveal" = "Aufdecken"; /* No comment provided by engineer. */ -"Revert" = "Zurückkehren"; +"Review conditions" = "Nutzungsbedingungen einsehen"; /* No comment provided by engineer. */ "Revoke" = "Widerrufen"; @@ -3236,40 +4320,44 @@ /* No comment provided by engineer. */ "Run chat" = "Chat starten"; +/* No comment provided by engineer. */ +"Safely receive files" = "Dateien sicher herunterladen"; + /* No comment provided by engineer. */ "Safer groups" = "Sicherere Gruppen"; -/* chat item action */ +/* 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. */ @@ -3278,15 +4366,15 @@ /* 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"; @@ -3305,6 +4393,15 @@ /* 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"; @@ -3320,6 +4417,9 @@ /* 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"; @@ -3327,11 +4427,14 @@ "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 fügen Sie den SimpleX-Link ein"; +"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"; @@ -3341,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"; @@ -3350,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"; @@ -3377,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"; @@ -3410,7 +4534,7 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Bis zu 100 der letzten Nachrichten an neue Gruppenmitglieder senden."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Der Absender hat die Dateiübertragung abgebrochen."; /* No comment provided by engineer. */ @@ -3446,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"; @@ -3464,24 +4630,48 @@ /* 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"; @@ -3506,30 +4696,58 @@ /* No comment provided by engineer. */ "Settings" = "Einstellungen"; +/* alert message */ +"Settings were changed." = "Die Einstellungen wurden geändert."; + /* No comment provided by engineer. */ "Shape profile images" = "Form der Profil-Bilder"; -/* chat item action */ +/* 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 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"; @@ -3539,6 +4757,12 @@ /* 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"; @@ -3548,12 +4772,27 @@ /* 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."; @@ -3570,7 +4809,7 @@ "SimpleX links" = "SimpleX-Links"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "In dieser Gruppe sind SimpleX-Links nicht erlaubt."; +"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"; @@ -3590,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"; @@ -3603,14 +4848,36 @@ "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. */ "Square, circle, or anything in between." = "Quadratisch, kreisförmig oder irgendetwas dazwischen."; @@ -3626,18 +4893,21 @@ /* 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" = "Chat beenden"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Chat beenden, um Datenbankaktionen zu erlauben"; - /* 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."; @@ -3645,18 +4915,18 @@ "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 */ @@ -3665,27 +4935,54 @@ /* 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"; /* No comment provided by engineer. */ "Tap button " = "Schaltfläche antippen "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Zum Aktivieren des Profils tippen."; @@ -3705,11 +5002,14 @@ "Tap to scan" = "Zum Scannen tippen"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Zum Starten eines neuen Chats tippen"; +"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"; @@ -3719,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. */ @@ -3741,10 +5047,13 @@ "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."; +"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 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."; +"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."; @@ -3752,6 +5061,9 @@ /* 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!"; @@ -3764,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."; @@ -3777,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! ✅"; @@ -3792,13 +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 **%@**." = "Mögliche Server für neue 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. */ +"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. */ "The text you pasted is not a SimpleX link." = "Der von Ihnen eingefügte Text ist kein SimpleX-Link."; /* No comment provided by engineer. */ -"Theme" = "Design"; +"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 **%@**."; @@ -3807,13 +5140,16 @@ "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."; @@ -3842,9 +5178,21 @@ /* 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. */ +"Title" = "Bezeichnung"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Um Fragen zu stellen und aktuelle Informationen zu erhalten:"; @@ -3858,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."; @@ -3866,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: %@)."; @@ -3920,13 +5304,13 @@ /* rcv group event chat item */ "unblocked %@" = "%@ wurde freigegeben"; -/* item status description */ -"Unexpected error: %@" = "Unerwarteter Fehler: %@"; +/* 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. */ @@ -3953,6 +5337,12 @@ /* 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"; @@ -3974,21 +5364,24 @@ /* 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. */ +"Unsupported connection link" = "Verbindungs-Link wird nicht unterstützt"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Bis zu 100 der letzten Nachrichten werden an neue Mitglieder gesendet."; /* No comment provided by engineer. */ "Update" = "Aktualisieren"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Einstellung für .onion-Hosts aktualisieren?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Datenbank-Passwort aktualisieren"; @@ -3996,7 +5389,10 @@ "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"; @@ -4008,10 +5404,10 @@ "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"; @@ -4019,17 +5415,32 @@ /* 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" = "Das aktuelle Profil nutzen"; +"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"; @@ -4041,25 +5452,52 @@ "Use iOS call interface" = "iOS Anrufschnittstelle nutzen"; /* No comment provided by engineer. */ -"Use new incognito profile" = "Ein neues Inkognito-Profil nutzen"; +"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. */ +"Use SOCKS proxy" = "SOCKS-Proxy nutzen"; + +/* No comment provided by engineer. */ +"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. */ -"User profile" = "Benutzerprofil"; +"Use the app with one hand." = "Die App mit einer Hand bedienen."; /* 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 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."; @@ -4109,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"; @@ -4116,17 +5557,23 @@ "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"; @@ -4140,7 +5587,7 @@ "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"; @@ -4166,6 +5613,12 @@ /* 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!"; @@ -4200,7 +5653,10 @@ "When connecting audio and video calls." = "Bei der Verbindung über Audio- und Video-Anrufe."; /* 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 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."; @@ -4223,14 +5679,26 @@ /* 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"; @@ -4238,9 +5706,6 @@ /* No comment provided by engineer. */ "you" = "Profil"; -/* No comment provided by engineer. */ -"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."; @@ -4256,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."; @@ -4286,6 +5754,9 @@ /* 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"; @@ -4295,6 +5766,12 @@ /* 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 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"; @@ -4314,7 +5791,13 @@ "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."; @@ -4326,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."; @@ -4337,7 +5820,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:"; -/* No comment provided by engineer. */ +/* 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. */ @@ -4356,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!"; @@ -4367,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."; @@ -4385,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."; @@ -4406,6 +5895,9 @@ /* 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"; @@ -4430,6 +5922,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this 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 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."; @@ -4445,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"; @@ -4457,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 (%@)."; @@ -4472,6 +5967,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Ihre Kontakte bleiben weiterhin verbunden."; +/* No comment provided by engineer. */ +"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."; @@ -4485,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"; @@ -4494,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."; @@ -4503,10 +6004,10 @@ "Your random profile" = "Ihr Zufallsprofil"; /* No comment provided by engineer. */ -"Your server" = "Ihr Server"; +"Your server address" = "Ihre Serveradresse"; /* No comment provided by engineer. */ -"Your server address" = "Ihre Serveradresse"; +"Your servers" = "Ihre Server"; /* No comment provided by engineer. */ "Your settings" = "Einstellungen"; @@ -4514,9 +6015,3 @@ /* 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 8a61d6c438..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)"; @@ -31,30 +16,15 @@ /* 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. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(nuevo)"; /* No comment provided by engineer. */ "(this device v%@)" = "(este dispositivo v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* No comment provided by engineer. */ "[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Contribuye](https://github.com/simplex-chat/simplex-chat#contribute)"; @@ -65,10 +35,7 @@ "[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 contact**: to create a new invitation link, or connect via a link you received." = "**Añadir contacto**: crea un enlace de invitación nuevo o usa un enlace recibido."; - -/* 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 group**: to create a new group." = "**Crear grupo**: crea un grupo nuevo."; @@ -80,19 +47,22 @@ "**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. */ "**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 / 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."; @@ -155,7 +125,10 @@ "%@ 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"; @@ -163,6 +136,9 @@ /* 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"; @@ -173,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"; @@ -229,9 +223,6 @@ /* 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"; @@ -277,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "un dia"; /* time interval */ @@ -286,12 +278,23 @@ /* 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 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"; @@ -308,13 +311,13 @@ "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"; @@ -326,10 +329,7 @@ "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"; @@ -338,54 +338,107 @@ "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 contact" = "Añadir contacto"; +"Add friends" = "Añadir amigos"; /* No comment provided by engineer. */ -"Add preset servers" = "Añadir servidores predefinidos"; +"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"; @@ -401,21 +454,33 @@ /* 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 data is kept private on your device." = "Todos los datos son privados y están en tu dispositivo."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados."; @@ -423,14 +488,26 @@ "all members" = "todos los miembros"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone!" = "Todos los mensajes serán borrados. ¡No podrá deshacerse!"; +"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! The messages will be deleted ONLY for you." = "Se eliminarán todos los mensajes SOLO para tí. ¡No podrá deshacerse!"; +"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."; @@ -446,9 +523,15 @@ /* 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 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)"; @@ -464,14 +547,20 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Permites el envío de mensajes temporales."; +/* No comment provided by engineer. */ +"Allow sharing" = "Permitir compartir"; + /* No comment provided by engineer. */ "Allow 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." = "Permitir enviar enlaces SimpleX."; +"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."; @@ -509,6 +598,9 @@ /* 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"; @@ -518,9 +610,15 @@ /* 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: %@"; @@ -531,7 +629,10 @@ "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"; @@ -539,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"; @@ -551,15 +655,48 @@ /* 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"; @@ -599,9 +736,15 @@ /* 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"; @@ -617,12 +760,39 @@ /* 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"; @@ -647,12 +817,19 @@ /* rcv group event chat item */ "blocked %@" = "ha bloqueado a %@"; -/* marked deleted chat item preview text */ +/* 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"; @@ -675,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!"; @@ -692,9 +884,18 @@ /* No comment provided by engineer. */ "Calls" = "Llamadas"; +/* No comment provided by engineer. */ +"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!"; @@ -702,6 +903,10 @@ "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. */ @@ -714,14 +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?"; @@ -747,7 +964,7 @@ "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 */ @@ -766,28 +983,43 @@ "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á parado"; +"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." = "Chat parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar Chat."; +"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!"; @@ -795,10 +1027,31 @@ /* 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. */ @@ -814,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. */ @@ -823,16 +1085,28 @@ "Clear conversation?" = "¿Vaciar conversación?"; /* No comment provided by engineer. */ -"Clear private notes?" = "¿Borrar notas privadas?"; +"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. */ -"colored" = "coloreado"; +"Color chats with the new themes." = "Colorea los chats con los nuevos temas."; /* No comment provided by engineer. */ -"Colors" = "Colores"; +"Color mode" = "Modo de color"; + +/* No comment provided by engineer. */ +"colored" = "coloreado"; + +/* report reason */ +"Community guidelines violation" = "Violación de las normas de la comunidad"; /* server test step */ "Compare file" = "Comparar archivo"; @@ -843,15 +1117,48 @@ /* 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"; @@ -865,11 +1172,14 @@ "Confirm password" = "Confirmar contraseña"; /* No comment provided by engineer. */ -"Confirm that you remember database passphrase to migrate it." = "Confirma que recuerdas la frase de contraseña de la base de datos para migrarla."; +"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"; @@ -885,6 +1195,9 @@ /* 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?"; @@ -909,17 +1222,26 @@ /* 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. */ +"Connected servers" = "Servidores conectados"; + /* No comment provided by engineer. */ "Connected to desktop" = "Conectado con ordenador"; /* No comment provided by engineer. */ -"connecting" = "conectando"; +"connecting" = "conectando..."; + +/* No comment provided by engineer. */ +"Connecting" = "Conectando"; /* No comment provided by engineer. */ "connecting (accepted)" = "conectando (aceptado)"; @@ -942,36 +1264,66 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Conectando con el servidor... (error: %@)"; +/* 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"; -/* chat list item title */ +/* 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 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 expirado"; +"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$@"; @@ -981,6 +1333,9 @@ /* 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"; @@ -994,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"; @@ -1002,21 +1357,36 @@ /* 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 %@?"; @@ -1024,10 +1394,10 @@ "Create" = "Crear"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Crear grupo usando perfil aleatorio."; +"Create 1-time link" = "Crear enlace de un uso"; /* 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 a group using a random profile." = "Crear grupo usando perfil aleatorio."; /* server test step */ "Create file" = "Crear archivo"; @@ -1041,6 +1411,9 @@ /* 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/). 💻"; @@ -1059,15 +1432,15 @@ /* No comment provided by engineer. */ "Create your profile" = "Crea tu perfil"; +/* No comment provided by engineer. */ +"Created" = "Creadas"; + /* No comment provided by engineer. */ "Created at" = "Creado"; /* copied message info */ "Created at: %@" = "Creado: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Creado en %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Creando enlace al archivo"; @@ -1077,12 +1450,18 @@ /* 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 %@."; @@ -1092,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"; @@ -1132,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."; @@ -1155,26 +1543,37 @@ /* 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?" = "¿Elimina %lld mensajes?"; +"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"; @@ -1192,10 +1591,10 @@ "Delete and notify contact" = "Eliminar y notificar contacto"; /* No comment provided by engineer. */ -"Delete archive" = "Eliminar archivo"; +"Delete chat" = "Eliminar chat"; /* No comment provided by engineer. */ -"Delete chat archive?" = "¿Eliminar archivo del chat?"; +"Delete chat messages from your device." = "Elimina los mensajes del dispositivo."; /* No comment provided by engineer. */ "Delete chat profile" = "Eliminar perfil"; @@ -1203,6 +1602,9 @@ /* No comment provided by engineer. */ "Delete chat profile?" = "¿Eliminar perfil?"; +/* No comment provided by engineer. */ +"Delete chat?" = "¿Eliminar chat?"; + /* No comment provided by engineer. */ "Delete connection" = "Eliminar conexión"; @@ -1210,10 +1612,7 @@ "Delete contact" = "Eliminar contacto"; /* No comment provided by engineer. */ -"Delete Contact" = "Eliminar contacto"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "¿Eliminar contacto?\n¡No podrá deshacerse!"; +"Delete contact?" = "¿Eliminar contacto?"; /* No comment provided by engineer. */ "Delete database" = "Eliminar base de datos"; @@ -1225,7 +1624,7 @@ "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"; @@ -1251,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"; @@ -1270,7 +1672,7 @@ "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 conexión pendiente?"; @@ -1281,12 +1683,24 @@ /* 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"; @@ -1297,7 +1711,13 @@ "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"; @@ -1320,9 +1740,27 @@ /* 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"; @@ -1348,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"; @@ -1362,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"; @@ -1372,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á"; @@ -1384,7 +1834,7 @@ "Disconnect" = "Desconectar"; /* No comment provided by engineer. */ -"Disconnect desktop?" = "¿Desconectar ordenador?"; +"Disconnect desktop?" = "¿Desconectar del ordenador?"; /* No comment provided by engineer. */ "Discover and join groups" = "Descubre y únete a grupos"; @@ -1396,11 +1846,23 @@ "Do it later" = "Hacer más tarde"; /* No comment provided by engineer. */ -"Do not send history to new members." = "No enviar historial a miembros nuevos."; +"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"; @@ -1408,20 +1870,39 @@ "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"; -/* chat item action */ +/* 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"; @@ -1434,12 +1915,18 @@ /* 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"; @@ -1452,12 +1939,15 @@ /* 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"; @@ -1492,7 +1982,10 @@ "enabled" = "activado"; /* No comment provided by engineer. */ -"Enabled for" = "Activar para"; +"Enabled" = "Activado"; + +/* No comment provided by engineer. */ +"Enabled for" = "Activado para"; /* enabled status */ "enabled for contact" = "activado para el contacto"; @@ -1507,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"; @@ -1566,6 +2059,9 @@ /* 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"; @@ -1591,7 +2087,7 @@ "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…"; @@ -1614,24 +2110,39 @@ /* 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 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"; /* 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"; @@ -1641,6 +2152,9 @@ /* 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"; @@ -1650,6 +2164,9 @@ /* 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"; @@ -1662,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"; @@ -1692,26 +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 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"; -/* No comment provided by engineer. */ +/* 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"; @@ -1725,6 +2260,9 @@ /* 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"; @@ -1750,20 +2288,29 @@ "Error starting chat" = "Error al iniciar Chat"; /* No comment provided by engineer. */ -"Error stopping chat" = "Error al parar 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"; @@ -1779,7 +2326,9 @@ /* No comment provided by engineer. */ "Error: " = "Error: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Error: %@"; /* No comment provided by engineer. */ @@ -1789,10 +2338,13 @@ "Error: URL is invalid" = "Error: la URL no es válida"; /* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Incluso si está desactivado para la conversación."; +"Errors" = "Errores"; + +/* servers error */ +"Errors in servers configuration." = "Error en la configuración del servidor."; /* No comment provided by engineer. */ -"event happened" = "evento ocurrido"; +"Even when disabled in the conversation." = "Incluso si está desactivado para la conversación."; /* No comment provided by engineer. */ "Exit without saving" = "Salir sin guardar"; @@ -1800,12 +2352,21 @@ /* chat item action */ "Expand" = "Expandir"; +/* No comment provided by engineer. */ +"expired" = "expirados"; + +/* token status text */ +"Expired" = "Expirado"; + /* No comment provided by engineer. */ "Export database" = "Exportar base de datos"; /* 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."; @@ -1821,12 +2382,42 @@ /* No comment provided by engineer. */ "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."; @@ -1834,11 +2425,14 @@ "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 el 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"; @@ -1846,7 +2440,7 @@ "Files and media" = "Archivos y multimedia"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Los archivos y multimedia no están permitidos 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"; @@ -1887,15 +2481,45 @@ /* 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"; @@ -1905,6 +2529,24 @@ /* 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"; @@ -1918,20 +2560,26 @@ "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 decentralized – visible only to members." = "Completamente descentralizado: sólo visible a 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"; @@ -1968,27 +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. (24 hours)" = "Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)"; - -/* 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 SimpleX links." = "Los miembros del grupo pueden enviar enlaces SimpleX."; - -/* 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:"; @@ -2011,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"; @@ -2050,6 +2683,12 @@ "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. */ @@ -2071,7 +2710,7 @@ "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."; +"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!"; @@ -2089,13 +2728,13 @@ "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 el 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"; @@ -2109,9 +2748,15 @@ /* 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"; @@ -2122,7 +2767,7 @@ "Improved server configuration" = "Configuración del servidor mejorada"; /* No comment provided by engineer. */ -"In order to continue, chat should be stopped." = "Para continuar, Chat debe estar parado."; +"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"; @@ -2130,6 +2775,15 @@ /* 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"; @@ -2184,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"; @@ -2223,7 +2895,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Respuesta no válida"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "¡Dirección de servidor no válida!"; /* item status text */ @@ -2235,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"; @@ -2262,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"; @@ -2269,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."; @@ -2280,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." = "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 (%@)."; @@ -2292,7 +2976,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Interfáz en japonés"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Unirte"; /* No comment provided by engineer. */ @@ -2319,13 +3003,16 @@ /* No comment provided by engineer. */ "Joining group" = "Entrando al grupo"; -/* No comment provided by engineer. */ +/* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "¿Guardar invitación no usada?"; /* No comment provided by engineer. */ @@ -2343,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"; @@ -2373,6 +3066,15 @@ /* 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"; @@ -2382,14 +3084,11 @@ /* No comment provided by engineer. */ "Live messages" = "Mensajes en vivo"; -/* No comment provided by engineer. */ -"Local" = "Local"; - /* No comment provided by engineer. */ "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"; @@ -2397,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"; @@ -2433,6 +3123,12 @@ /* 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"; @@ -2445,6 +3141,15 @@ /* rcv group event chat item */ "member connected" = "conectado"; +/* 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 \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\" y todos serán notificados."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará al grupo."; @@ -2452,7 +3157,43 @@ "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 group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No podrá deshacerse!"; +"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"; @@ -2460,9 +3201,21 @@ /* 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 mensajes"; @@ -2470,14 +3223,29 @@ "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"; @@ -2493,6 +3261,18 @@ /* 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."; @@ -2533,7 +3313,7 @@ "Migration is completed" = "Migración completada"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migraciones: %@"; +"Migrations:" = "Migraciones:"; /* time unit */ "minutes" = "minutos"; @@ -2556,31 +3336,40 @@ /* 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. */ @@ -2589,39 +3378,57 @@ /* 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"; @@ -2637,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"; @@ -2646,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"; @@ -2658,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"; @@ -2670,27 +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\")"; @@ -2698,11 +3586,11 @@ "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"; /* feature offered item */ @@ -2711,7 +3599,7 @@ /* feature offered item */ "offered %@: %@" = "ofrecido %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2720,26 +3608,29 @@ /* No comment provided by engineer. */ "Old database" = "Base de datos antigua"; -/* No comment provided by engineer. */ -"Old database archive" = "Archivo de bases de datos antiguas"; - /* 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 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."; @@ -2750,6 +3641,12 @@ /* 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."; @@ -2780,15 +3677,21 @@ /* 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"; /* authentication reason */ "Open chat console" = "Abrir consola de Chat"; +/* No comment provided by engineer. */ +"Open conditions" = "Abrir condiciones"; + /* No comment provided by engineer. */ "Open group" = "Grupo abierto"; @@ -2798,30 +3701,48 @@ /* No comment provided by engineer. */ "Open Settings" = "Abrir Configuración"; -/* authentication reason */ -"Open user profiles" = "Abrir perfil de usuario"; - -/* 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."; - /* No comment provided by engineer. */ "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 escanear código QR"; +"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 mostrar este código"; +"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"; @@ -2843,6 +3764,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "¡Código de acceso guardado!"; +/* No comment provided by engineer. */ +"Password" = "Contraseña"; + /* No comment provided by engineer. */ "Password to show" = "Contraseña para hacerlo visible"; @@ -2859,16 +3783,22 @@ "Paste link to connect!" = "Pegar enlace para conectar!"; /* No comment provided by engineer. */ -"Paste the link you received" = "Pegar el enlace recibido"; +"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"; @@ -2882,9 +3812,18 @@ /* 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."; @@ -2924,55 +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. */ +"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. */ -"Profile and server connections" = "Datos del perfil y conexiones"; +"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" = "Imágenes del perfil"; - -/* No comment provided by engineer. */ -"Profile name" = "Nombre del perfil"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nombre del perfil:"; +"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. */ @@ -2987,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."; @@ -2997,25 +3975,40 @@ "Prohibit sending files and media." = "No permitir el envío de archivos y multimedia."; /* No comment provided by engineer. */ -"Prohibit sending SimpleX links." = "No permitir el envío de enlaces SimpleX."; +"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"; @@ -3029,33 +4022,36 @@ /* 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)." = "Saber más en el [Manual del 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/chat-profiles.html#incognito-mode)." = "Saber más en [Guía de Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Saber más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; /* No comment provided by engineer. */ -"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Saber más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; - -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Saber más 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…"; @@ -3075,10 +4071,16 @@ "Received message" = "Mensaje entrante"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea."; +"Received messages" = "Mensajes recibidos"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Concurrencia en la recepción"; +"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."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Se detendrá la recepción del archivo."; @@ -3095,9 +4097,24 @@ /* 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?"; @@ -3110,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. */ @@ -3119,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."; @@ -3131,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"; @@ -3150,7 +4186,7 @@ "removed contact address" = "dirección de contacto eliminada"; /* profile update event chat item */ -"removed profile picture" = "imagen de perfil eliminada"; +"removed profile picture" = "ha eliminado la imagen del perfil"; /* rcv group event chat item */ "removed you" = "te ha expulsado"; @@ -3182,17 +4218,65 @@ /* 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"; @@ -3219,7 +4303,7 @@ "Reveal" = "Revelar"; /* No comment provided by engineer. */ -"Revert" = "Revertir"; +"Review conditions" = "Revisar condiciones"; /* No comment provided by engineer. */ "Revoke" = "Revocar"; @@ -3234,42 +4318,46 @@ "Role" = "Rol"; /* No comment provided by engineer. */ -"Run chat" = "Ejecutar chat"; +"Run chat" = "Ejecutar SimpleX"; + +/* No comment provided by engineer. */ +"Safely receive files" = "Recibe archivos de forma segura"; /* No comment provided by engineer. */ "Safer groups" = "Grupos más seguros"; -/* chat item action */ +/* 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 grupo"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Guardar y reconectar"; + /* No comment provided by engineer. */ "Save and update group profile" = "Guardar y actualizar perfil del grupo"; -/* No comment provided by engineer. */ -"Save archive" = "Guardar archivo"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Guardar configuración de auto aceptar"; - /* 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. */ @@ -3278,15 +4366,15 @@ /* 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"; @@ -3305,6 +4393,15 @@ /* 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"; @@ -3318,7 +4415,10 @@ "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"; @@ -3332,6 +4432,9 @@ /* network option */ "sec" = "seg"; +/* No comment provided by engineer. */ +"Secondary" = "Secundario"; + /* time unit */ "seconds" = "segundos"; @@ -3341,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"; @@ -3350,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"; @@ -3378,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"; @@ -3408,9 +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." = "Enviar hasta 100 últimos mensajes a los miembros nuevos."; +"Send up to 100 last messages to new members." = "Se envían hasta 100 mensajes más recientes a los miembros nuevos."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "El remitente ha cancelado la transferencia de archivos."; /* No comment provided by engineer. */ @@ -3446,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"; @@ -3462,31 +4628,55 @@ "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" = "nueva imagen de perfil"; +"set new profile picture" = "tiene nueva imagen del perfil"; /* No comment provided by engineer. */ "Set passcode" = "Código autodestrucción"; @@ -3506,30 +4696,58 @@ /* No comment provided by engineer. */ "Settings" = "Configuración"; +/* 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"; -/* chat item action */ +/* 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 this 1-time invite link" = "Compartir este enlace 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"; @@ -3539,6 +4757,12 @@ /* 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"; @@ -3548,12 +4772,27 @@ /* 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."; @@ -3570,7 +4809,7 @@ "SimpleX links" = "Enlaces SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "Los enlaces SimpleX no se permiten en este grupo."; +"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"; @@ -3590,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"; @@ -3600,17 +4845,39 @@ "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. */ "Square, circle, or anything in between." = "Cuadrada, circular o cualquier forma intermedia."; @@ -3626,20 +4893,23 @@ /* 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. */ +"Statistics" = "Estadísticas"; + /* No comment provided by engineer. */ "Stop" = "Parar"; /* No comment provided by engineer. */ -"Stop chat" = "Parar chat"; +"Stop chat" = "Parar SimpleX"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Para habilitar las acciones sobre la base de datos, debes parar 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." = "Para poder exportar, importar o eliminar la base de datos primero debes parar Chat. Mientras tanto no podrás recibir ni enviar mensajes."; +"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?"; @@ -3653,10 +4923,10 @@ /* 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 */ @@ -3665,27 +4935,54 @@ /* 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."; @@ -3699,16 +4996,19 @@ "Tap to join incognito" = "Pulsa para unirte en modo incógnito"; /* No comment provided by engineer. */ -"Tap to paste link" = "Pulsa para pegar enlace"; +"Tap to paste link" = "Pulsa para pegar el enlacePulsa para pegar enlace"; /* No comment provided by engineer. */ "Tap to scan" = "Pulsa para escanear"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Pulsa para iniciar chat nuevo"; +"TCP connection" = "Conexión TCP"; /* No comment provided by engineer. */ -"TCP connection timeout" = "Tiempo de espera de la conexión TCP agotado"; +"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"; @@ -3719,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"; @@ -3728,8 +5034,8 @@ /* 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!"; @@ -3741,16 +5047,22 @@ "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: por favor, abre la configuración para activarlo."; +"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 SimpleX."; +"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á!"; @@ -3764,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."; @@ -3777,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! ✅"; @@ -3792,19 +5116,28 @@ "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. */ -"The text you pasted is not a SimpleX link." = "El texto pegado no es un enlace SimpleX."; +"The servers for new files of your current chat profile **%@**." = "Servidores para enviar archivos en tu perfil **%@**."; /* No comment provided by engineer. */ -"Theme" = "Tema"; +"The text you pasted is not a SimpleX link." = "El texto pegado no es un enlace de SimpleX."; + +/* No comment provided by engineer. */ +"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 es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán."; @@ -3812,6 +5145,9 @@ /* 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 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 es irreversible. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente."; @@ -3842,9 +5178,21 @@ /* 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:"; @@ -3858,7 +5206,7 @@ "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 tu 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."; @@ -3866,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." = "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."; /* 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 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: %@)."; @@ -3912,7 +5296,7 @@ "Unblock member" = "Desbloquear miembro"; /* No comment provided by engineer. */ -"Unblock member for all?" = "¿Desbloquear miembro para todos?"; +"Unblock member for all?" = "¿Desbloquear el miembro para todos?"; /* No comment provided by engineer. */ "Unblock member?" = "¿Desbloquear miembro?"; @@ -3920,13 +5304,13 @@ /* rcv group event chat item */ "unblocked %@" = "ha desbloqueado a %@"; -/* item status description */ -"Unexpected error: %@" = "Error inesperado: %@"; +/* 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. */ @@ -3953,6 +5337,12 @@ /* 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"; @@ -3960,7 +5350,7 @@ "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"; @@ -3974,21 +5364,24 @@ /* 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. */ +"Unsupported connection link" = "Enlace de conexión no compatible"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Hasta 100 últimos mensajes son enviados a los miembros nuevos."; /* No comment provided by engineer. */ "Update" = "Actualizar"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "¿Actualizar la configuración de los hosts .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Actualizar contraseña de la base de datos"; @@ -3996,7 +5389,10 @@ "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"; @@ -4005,26 +5401,35 @@ "updated profile" = "perfil actualizado"; /* 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."; - -/* 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"; @@ -4032,7 +5437,13 @@ "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"; @@ -4046,20 +5457,47 @@ /* 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. */ +"Use SOCKS proxy" = "Usar proxy SOCKS"; + +/* No comment provided by engineer. */ +"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. */ -"User profile" = "Perfil de usuario"; +"Use the app with one hand." = "Usa la aplicación con una sola mano."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Usar hosts .onion requiere un proveedor VPN compatible."; +"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."; @@ -4109,6 +5547,9 @@ /* 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"; @@ -4119,14 +5560,20 @@ "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 el 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"; @@ -4140,7 +5587,7 @@ "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"; @@ -4166,6 +5613,12 @@ /* 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!"; @@ -4200,7 +5653,10 @@ "When connecting audio and video calls." = "Al iniciar llamadas de audio y vídeo."; /* 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 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."; @@ -4223,14 +5679,26 @@ /* 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í"; @@ -4238,9 +5706,6 @@ /* No comment provided by engineer. */ "you" = "tu"; -/* No comment provided by engineer. */ -"You" = "Tú"; - /* 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."; @@ -4254,7 +5719,10 @@ "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 %@."; @@ -4286,6 +5754,9 @@ /* No comment provided by engineer. */ "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"; @@ -4295,6 +5766,12 @@ /* 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 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"; @@ -4314,7 +5791,13 @@ "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."; @@ -4326,10 +5809,10 @@ "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 código QR para que cualquiera pueda 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."; @@ -4337,7 +5820,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Puedes usar la sintaxis markdown para dar formato a tus mensajes:"; -/* No comment provided by engineer. */ +/* 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. */ @@ -4356,10 +5839,10 @@ "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."; +"You could not be verified; please try again." = "No has podido ser autenticado. Inténtalo de nuevo."; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "No has podido ser autenticado. Inténtalo de nuevo."; +"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!"; @@ -4367,9 +5850,6 @@ /* 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 no chats" = "No tienes chats"; - /* 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."; @@ -4385,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."; @@ -4406,23 +5895,26 @@ /* chat list item description */ "you shared one-time link incognito" = "has compartido enlace de un solo uso en modo incógnito"; +/* 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 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."; +"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 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 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 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 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 compruébalo más tarde."; +"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á identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano."; +"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."; @@ -4430,6 +5922,9 @@ /* 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á."; @@ -4445,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" = "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 %@ servers" = "Mis servidores %@"; - /* No comment provided by engineer. */ "Your calls" = "Llamadas"; @@ -4457,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)." = "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)."; +"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 (%@)."; @@ -4472,6 +5967,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Tus contactos permanecerán conectados."; +/* No comment provided by engineer. */ +"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."; @@ -4491,10 +5989,13 @@ "Your profile" = "Tu perfil"; /* No comment provided by engineer. */ -"Your profile **%@** will be shared." = "Tu perfil **%@** será compartido."; +"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.\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 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."; @@ -4503,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"; @@ -4514,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/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index c71f9e089c..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,9 +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. */ "**e2e encrypted** audio call" = "**e2e-salattu** äänipuhelu"; @@ -62,16 +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)."; +"**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."; +"**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."; @@ -121,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!"; @@ -175,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"; @@ -220,7 +181,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 päivä"; /* time interval */ @@ -229,10 +191,12 @@ /* 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. */ @@ -268,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. */ @@ -293,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 */ @@ -302,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."; @@ -431,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: %@"; @@ -551,7 +508,8 @@ /* 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 */ @@ -560,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. */ @@ -591,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 */ @@ -609,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"; @@ -634,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. */ @@ -648,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. */ @@ -663,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"; @@ -735,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. */ @@ -777,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"; @@ -795,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. */ @@ -804,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"; @@ -819,6 +768,9 @@ /* No comment provided by engineer. */ "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 profile" = "Luo profiilisi"; + /* server test step */ "Create queue" = "Luo jono"; @@ -831,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"; @@ -921,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. */ @@ -930,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. */ @@ -945,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"; @@ -963,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"; @@ -1005,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. */ @@ -1017,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?"; @@ -1084,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)"; @@ -1108,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"; @@ -1167,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. */ @@ -1299,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ä"; @@ -1338,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"; @@ -1371,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"; @@ -1413,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. */ @@ -1434,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. */ @@ -1446,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"; @@ -1470,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. */ @@ -1492,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!"; @@ -1536,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!"; @@ -1578,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. (24 hours)" = "Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)"; - -/* 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:"; @@ -1653,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"; @@ -1696,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"; @@ -1765,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ä"; @@ -1785,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 */ @@ -1831,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."; @@ -1854,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. */ @@ -1884,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. */ @@ -1914,9 +1816,6 @@ /* No comment provided by engineer. */ "Live messages" = "Live-viestit"; -/* No comment provided by engineer. */ -"Local" = "Paikallinen"; - /* No comment provided by engineer. */ "Local name" = "Paikallinen nimi"; @@ -1929,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"; @@ -1983,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"; @@ -1999,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"; @@ -2026,7 +1934,7 @@ "Migration is completed" = "Siirto on valmis"; /* No comment provided by engineer. */ -"Migrations: %@" = "Siirrot: %@"; +"Migrations:" = "Siirrot:"; /* time unit */ "minutes" = "minuuttia"; @@ -2058,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. */ @@ -2082,7 +1987,7 @@ /* No comment provided by engineer. */ "Network status" = "Verkon tila"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "ei koskaan"; /* notification */ @@ -2091,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"; @@ -2151,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"; @@ -2170,11 +2078,11 @@ "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"; /* feature offered item */ @@ -2183,15 +2091,12 @@ /* 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ä"; @@ -2199,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."; @@ -2258,12 +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."; - /* member role */ "owner" = "omistaja"; @@ -2292,10 +2191,7 @@ "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"; @@ -2351,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"; @@ -2378,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. */ @@ -2426,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)."; @@ -2441,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ä"; @@ -2492,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. */ @@ -2576,9 +2467,6 @@ /* chat item action */ "Reveal" = "Paljasta"; -/* No comment provided by engineer. */ -"Revert" = "Palauta"; - /* No comment provided by engineer. */ "Revoke" = "Peruuta"; @@ -2594,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. */ @@ -2609,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"; @@ -2624,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. */ @@ -2633,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?"; @@ -2681,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. */ @@ -2705,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"; @@ -2720,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"; @@ -2732,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. */ @@ -2816,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. */ @@ -2825,7 +2700,7 @@ /* 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. */ @@ -2894,9 +2769,6 @@ /* 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."; @@ -2915,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."; @@ -2933,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 */ @@ -2972,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"; @@ -2996,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. */ @@ -3008,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."; @@ -3029,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."; @@ -3041,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! ✅"; @@ -3059,9 +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. */ "These settings are for your current profile **%@**." = "Nämä asetukset koskevat nykyistä profiiliasi **%@**."; @@ -3098,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."; @@ -3137,13 +2997,10 @@ /* 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. */ @@ -3182,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"; @@ -3242,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."; @@ -3303,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!"; @@ -3347,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."; @@ -3362,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"; @@ -3411,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."; @@ -3422,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"; @@ -3449,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."; @@ -3524,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"; @@ -3539,9 +3363,6 @@ /* 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 (%@)."; @@ -3570,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."; @@ -3578,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"; @@ -3590,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 5e6c9c1b40..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,7 @@ "[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 contact**: to create a new invitation link, or connect via a link you received." = "**Ajouter un contact** : pour créer un nouveau lien d'invitation ou vous connecter via un lien que vous avez reçu."; - -/* 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 group**: to create a new group." = "**Créer un groupe** : pour créer un nouveau groupe."; @@ -80,10 +47,10 @@ "**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. */ "**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é."; @@ -92,7 +59,10 @@ "**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 / 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."; @@ -154,6 +124,9 @@ /* 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 %@"; @@ -163,6 +136,9 @@ /* 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"; @@ -175,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"; @@ -187,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"; @@ -229,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"; @@ -277,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 jour"; /* time interval */ @@ -286,12 +278,23 @@ /* 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 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"; @@ -317,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"; @@ -326,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"; @@ -338,54 +338,107 @@ "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 contact" = "Ajouter le contact"; +"Add friends" = "Ajouter des amis"; /* No comment provided by engineer. */ -"Add preset servers" = "Ajouter des serveurs prédéfinis"; +"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"; @@ -401,27 +454,42 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Paramètres réseau avancés"; +/* No comment provided by engineer. */ +"Advanced settings" = "Paramètres avancés"; + /* chat item text */ "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."; /* 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 !"; @@ -431,6 +499,12 @@ /* 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."; @@ -446,9 +520,15 @@ /* 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 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)"; @@ -464,9 +544,15 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Autorise l’envoi de messages éphémères."; +/* No comment provided by engineer. */ +"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."; @@ -509,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"; @@ -518,9 +607,15 @@ /* 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 : %@"; @@ -539,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"; @@ -552,7 +650,34 @@ "Apply" = "Appliquer"; /* No comment provided by engineer. */ -"Archive and upload" = "Archiver et transférer"; +"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"; @@ -560,6 +685,9 @@ /* 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"; @@ -599,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"; @@ -617,12 +751,39 @@ /* 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"; @@ -647,12 +808,19 @@ /* rcv group event chat item */ "blocked %@" = "%@ bloqué"; -/* marked deleted chat item preview text */ +/* 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"; @@ -674,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é !"; @@ -692,9 +875,18 @@ /* No comment provided by engineer. */ "Calls" = "Appels"; +/* No comment provided by engineer. */ +"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 !"; @@ -702,6 +894,10 @@ "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. */ @@ -714,14 +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 ?"; @@ -747,7 +955,7 @@ "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 */ @@ -766,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"; @@ -777,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"; @@ -789,16 +1009,40 @@ /* 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"; +/* 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. */ @@ -814,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. */ @@ -822,6 +1075,12 @@ /* 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 ?"; @@ -829,10 +1088,16 @@ "Clear verification" = "Retirer la vérification"; /* No comment provided by engineer. */ -"colored" = "coloré"; +"Color chats with the new themes." = "Colorez vos discussions avec les nouveaux thèmes."; /* No comment provided by engineer. */ -"Colors" = "Couleurs"; +"Color mode" = "Mode de couleur"; + +/* No comment provided by engineer. */ +"colored" = "coloré"; + +/* report reason */ +"Community guidelines violation" = "Infraction aux règles communautaires"; /* server test step */ "Compare file" = "Comparer le fichier"; @@ -843,15 +1108,48 @@ /* 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"; @@ -870,6 +1168,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Confirmer la transmission"; +/* token status text */ +"Confirmed" = "Confirmé"; + /* server test step */ "Connect" = "Se connecter"; @@ -885,6 +1186,9 @@ /* 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 ?"; @@ -909,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)"; @@ -942,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"; @@ -960,18 +1282,39 @@ /* 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$@"; @@ -981,6 +1324,9 @@ /* 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"; @@ -994,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"; @@ -1002,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 %@ ?"; @@ -1024,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." = "Vous pouvez créer une adresse pour permettre aux autres utilisateurs 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"; @@ -1041,6 +1402,9 @@ /* No comment provided by engineer. */ "Create link" = "Créer un lien"; +/* No comment provided by engineer. */ +"Create list" = "Créer une liste"; + /* 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/). 💻"; @@ -1059,15 +1423,15 @@ /* No comment provided by engineer. */ "Create your profile" = "Créez votre profil"; +/* No comment provided by engineer. */ +"Created" = "Créées"; + /* No comment provided by engineer. */ "Created at" = "Créé à"; /* copied message info */ "Created at: %@" = "Créé à : %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Créé le %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Création d'un lien d'archive"; @@ -1077,12 +1441,18 @@ /* 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 %@."; @@ -1092,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"; @@ -1155,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. */ @@ -1170,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 ?"; @@ -1192,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"; @@ -1203,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"; @@ -1210,10 +1603,7 @@ "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"; @@ -1251,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. */ @@ -1270,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 ?"; @@ -1281,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é à"; @@ -1299,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"; @@ -1306,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"; @@ -1320,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"; @@ -1348,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"; @@ -1362,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"; @@ -1372,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"; @@ -1398,30 +1839,61 @@ /* 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"; -/* chat item action */ +/* 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"; @@ -1434,12 +1906,18 @@ /* 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"; @@ -1452,12 +1930,15 @@ /* 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"; @@ -1491,6 +1972,9 @@ /* enabled status */ "enabled" = "activé"; +/* No comment provided by engineer. */ +"Enabled" = "Activé"; + /* No comment provided by engineer. */ "Enabled for" = "Activé pour"; @@ -1566,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é"; @@ -1614,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"; @@ -1641,6 +2143,9 @@ /* 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"; @@ -1650,6 +2155,9 @@ /* 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"; @@ -1662,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"; @@ -1692,26 +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"; +/* alert title */ +"Error loading servers" = "Erreur de chargement des serveurs"; + /* No comment provided by engineer. */ -"Error loading %@ servers" = "Erreur lors du chargement des serveurs %@"; +"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"; -/* No comment provided by engineer. */ +/* 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"; @@ -1725,6 +2251,9 @@ /* 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"; @@ -1753,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"; @@ -1779,7 +2317,9 @@ /* No comment provided by engineer. */ "Error: " = "Erreur : "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Erreur : %@"; /* No comment provided by engineer. */ @@ -1789,10 +2329,13 @@ "Error: URL is invalid" = "Erreur : URL invalide"; /* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Même s'il est désactivé dans la conversation."; +"Errors" = "Erreurs"; + +/* servers error */ +"Errors in servers configuration." = "Erreurs dans la configuration des serveurs."; /* No comment provided by engineer. */ -"event happened" = "event happened"; +"Even when disabled in the conversation." = "Même s'il est désactivé dans la conversation."; /* No comment provided by engineer. */ "Exit without saving" = "Quitter sans enregistrer"; @@ -1800,12 +2343,21 @@ /* chat item action */ "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"; /* 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."; @@ -1821,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."; @@ -1839,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"; @@ -1846,7 +2431,7 @@ "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"; @@ -1876,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."; @@ -1887,15 +2472,39 @@ /* 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é"; @@ -1905,6 +2514,24 @@ /* 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é"; @@ -1917,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."; @@ -1932,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"; @@ -1968,27 +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. (24 hours)" = "Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures)"; - -/* 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 SimpleX links." = "Les membres du groupe peuvent envoyer des liens SimpleX."; - -/* 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 :"; @@ -2050,7 +2659,10 @@ "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"; @@ -2095,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"; @@ -2109,9 +2721,15 @@ /* 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"; @@ -2130,6 +2748,9 @@ /* 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"; @@ -2184,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"; @@ -2223,7 +2847,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Réponse invalide"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Adresse de serveur invalide !"; /* item status text */ @@ -2235,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"; @@ -2262,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"; @@ -2269,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."; @@ -2280,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 (%@)."; @@ -2292,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. */ @@ -2319,13 +2955,16 @@ /* No comment provided by engineer. */ "Joining group" = "Entrain de rejoindre le groupe"; -/* No comment provided by engineer. */ +/* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Conserver l'invitation inutilisée ?"; /* No comment provided by engineer. */ @@ -2343,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"; @@ -2382,9 +3027,6 @@ /* No comment provided by engineer. */ "Live messages" = "Messages dynamiques"; -/* No comment provided by engineer. */ -"Local" = "Local"; - /* No comment provided by engineer. */ "Local name" = "Nom local"; @@ -2397,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"; @@ -2433,6 +3066,12 @@ /* 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"; @@ -2445,24 +3084,72 @@ /* 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"; @@ -2470,14 +3157,29 @@ "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"; @@ -2493,6 +3195,15 @@ /* 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."; @@ -2533,7 +3244,7 @@ "Migration is completed" = "La migration est terminée"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrations : %@"; +"Migrations:" = "Migrations :"; /* time unit */ "minutes" = "minutes"; @@ -2565,22 +3276,22 @@ /* 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. */ @@ -2589,39 +3300,54 @@ /* 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"; @@ -2637,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"; @@ -2658,6 +3393,9 @@ /* 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"; @@ -2670,27 +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\")"; @@ -2698,11 +3478,11 @@ "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"; /* feature offered item */ @@ -2711,7 +3491,7 @@ /* feature offered item */ "offered %@: %@" = "propose %1$@ : %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2720,9 +3500,6 @@ /* No comment provided by engineer. */ "Old database" = "Ancienne base de données"; -/* No comment provided by engineer. */ -"Old database archive" = "Archives de l'ancienne base de données"; - /* group pref value */ "on" = "on"; @@ -2730,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."; @@ -2780,15 +3563,21 @@ /* 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"; @@ -2798,15 +3587,18 @@ /* 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. */ -"Open-source protocol and code – anybody can run the servers." = "Protocole et code open-source – n'importe qui peut heberger un serveur."; - /* No comment provided by engineer. */ "Opening app…" = "Ouverture de l'app…"; +/* No comment provided by engineer. */ +"Operator" = "Opérateur"; + +/* alert title */ +"Operator server" = "Serveur de l'opérateur"; + +/* No comment provided by engineer. */ +"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"; @@ -2817,11 +3609,23 @@ "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 présenter ce code"; +"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"; @@ -2843,6 +3647,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "Code d'accès défini !"; +/* No comment provided by engineer. */ +"Password" = "Mot de passe"; + /* No comment provided by engineer. */ "Password to show" = "Mot de passe à entrer"; @@ -2865,10 +3672,10 @@ "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"; @@ -2882,9 +3689,18 @@ /* 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."; @@ -2927,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"; @@ -2934,26 +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"; @@ -2963,16 +3800,13 @@ /* No comment provided by engineer. */ "Profile images" = "Images de profil"; -/* No comment provided by engineer. */ -"Profile name" = "Nom du profil"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nom du 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. */ @@ -3005,15 +3839,30 @@ /* 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"; @@ -3029,20 +3878,23 @@ /* 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/chat-profiles.html#incognito-mode)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](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)."; @@ -3051,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…"; @@ -3075,10 +3927,16 @@ "Received message" = "Message 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."; +"Received messages" = "Messages reçus"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Réception simultanée"; +"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."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "La réception du fichier sera interrompue."; @@ -3095,11 +3953,26 @@ /* 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"; @@ -3110,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. */ @@ -3131,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"; @@ -3162,7 +4042,7 @@ "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 ?"; @@ -3182,18 +4062,36 @@ /* 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"; @@ -3219,7 +4117,7 @@ "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"; @@ -3236,30 +4134,31 @@ /* No comment provided by engineer. */ "Run chat" = "Exécuter le chat"; +/* No comment provided by engineer. */ +"Safely receive files" = "Réception de fichiers en toute sécurité"; + /* No comment provided by engineer. */ "Safer groups" = "Groupes plus sûrs"; -/* chat item action */ +/* alert button +chat item action */ "Save" = "Enregistrer"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Enregistrer (et en informer les contacts)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Enregistrer et en informer le contact"; /* No comment provided by engineer. */ "Save and notify group members" = "Enregistrer et en informer les membres du groupe"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Sauvegarder et se reconnecter"; + /* No comment provided by engineer. */ "Save and update group profile" = "Enregistrer et mettre à jour le profil du groupe"; -/* No comment provided by engineer. */ -"Save archive" = "Enregistrer l'archive"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Enregistrer les paramètres de validation automatique"; - /* No comment provided by engineer. */ "Save group profile" = "Enregistrer le profil du groupe"; @@ -3269,7 +4168,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Enregistrer la phrase secrète dans la Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Enregistrer les préférences ?"; /* No comment provided by engineer. */ @@ -3278,15 +4177,15 @@ /* No comment provided by engineer. */ "Save servers" = "Enregistrer les serveurs"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Enregistrer les serveurs ?"; -/* No comment provided by engineer. */ -"Save settings?" = "Enregistrer les paramètres ?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Enregistrer le message d'accueil ?"; +/* alert title */ +"Save your profile?" = "Sauvegarder votre profil ?"; + /* No comment provided by engineer. */ "saved" = "enregistré"; @@ -3305,6 +4204,15 @@ /* 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"; @@ -3320,6 +4228,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Scanner un code QR de serveur"; +/* No comment provided by engineer. */ +"search" = "rechercher"; + /* No comment provided by engineer. */ "Search" = "Rechercher"; @@ -3332,6 +4243,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secondaire"; + /* time unit */ "seconds" = "secondes"; @@ -3341,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é"; @@ -3350,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"; @@ -3377,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"; @@ -3387,16 +4310,25 @@ "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"; @@ -3410,7 +4342,7 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Envoi des 100 derniers messages aux nouveaux membres."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "L'expéditeur a annulé le transfert de fichiers."; /* No comment provided by engineer. */ @@ -3446,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"; @@ -3476,6 +4465,9 @@ /* 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"; @@ -3506,30 +4498,55 @@ /* No comment provided by engineer. */ "Settings" = "Paramètres"; +/* alert message */ +"Settings were changed." = "Les paramètres ont été modifiés."; + /* No comment provided by engineer. */ "Shape profile images" = "Images de profil modelable"; -/* chat item action */ +/* 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 this 1-time invite link" = "Partager ce 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"; @@ -3537,10 +4554,16 @@ "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" = "Afficher 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"; @@ -3548,12 +4571,24 @@ /* 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."; @@ -3570,7 +4605,7 @@ "SimpleX links" = "Liens SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "Les liens SimpleX sont interdits dans ce groupe."; +"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"; @@ -3590,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"; @@ -3603,11 +4644,29 @@ "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"; @@ -3626,18 +4685,21 @@ /* 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" = "Arrêter le chat"; -/* 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"; - /* 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é."; @@ -3653,10 +4715,10 @@ /* 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 */ @@ -3668,24 +4730,48 @@ /* 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."; @@ -3705,7 +4791,7 @@ "Tap to scan" = "Appuyez pour scanner"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Appuyez ici pour démarrer une nouvelle discussion"; +"TCP connection" = "Connexion TCP"; /* No comment provided by engineer. */ "TCP connection timeout" = "Délai de connexion TCP"; @@ -3719,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 %@."; @@ -3728,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. */ @@ -3741,10 +4830,13 @@ "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."; +"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 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."; +"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."; @@ -3752,6 +4844,9 @@ /* 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 !"; @@ -3764,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."; @@ -3777,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é ! ✅"; @@ -3794,11 +4901,20 @@ /* No comment provided by engineer. */ "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. */ +"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. */ "The text you pasted is not a SimpleX link." = "Le texte collé n'est pas un lien SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Thème"; +"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 **%@**."; @@ -3842,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."; @@ -3858,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."; @@ -3866,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 : %@)."; @@ -3920,13 +5075,13 @@ /* rcv group event chat item */ "unblocked %@" = "%@ débloqué"; -/* item status description */ -"Unexpected error: %@" = "Erreur inattendue : %@"; +/* 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. */ @@ -3953,6 +5108,12 @@ /* 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"; @@ -3974,10 +5135,13 @@ /* 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. */ @@ -3986,9 +5150,6 @@ /* No comment provided by engineer. */ "Update" = "Mise à jour"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Mettre à jour le paramètre des hôtes .onion ?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Mise à jour de la phrase secrète de la base de données"; @@ -3996,7 +5157,7 @@ "Update network settings?" = "Mettre à jour les paramètres réseau ?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Mettre à jour le mode d'isolement du transport ?"; +"Update settings?" = "Mettre à jour les paramètres ?"; /* rcv group event chat item */ "updated group profile" = "mise à jour du profil de groupe"; @@ -4008,16 +5169,22 @@ "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."; +"Upgrade and open chat" = "Mettre à niveau et ouvrir le chat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Mettre à niveau et ouvrir le chat"; +"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"; @@ -4025,12 +5192,21 @@ /* 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"; @@ -4046,20 +5222,35 @@ /* 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. */ +"Use SOCKS proxy" = "Utiliser un proxy SOCKS"; + /* No comment provided by engineer. */ "Use the app while in the call." = "Utiliser l'application pendant l'appel."; /* No comment provided by engineer. */ -"User profile" = "Profil d'utilisateur"; +"Use the app with one hand." = "Utiliser l'application d'une main."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "L'utilisation des hôtes .onion nécessite un fournisseur VPN compatible."; +"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."; @@ -4109,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"; @@ -4116,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 !"; @@ -4124,9 +5318,15 @@ /* 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"; @@ -4140,7 +5340,7 @@ "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"; @@ -4166,11 +5366,17 @@ /* 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"; +"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 !"; @@ -4200,7 +5406,10 @@ "When connecting audio and video calls." = "Lors des appels audio et vidéo."; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Vous pouvez accepter ou refuser les demandes de contacts."; +"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."; @@ -4223,14 +5432,26 @@ /* 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"; @@ -4238,9 +5459,6 @@ /* No comment provided by engineer. */ "you" = "vous"; -/* No comment provided by engineer. */ -"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."; @@ -4256,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 à %@."; @@ -4286,6 +5507,9 @@ /* 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"; @@ -4295,6 +5519,12 @@ /* 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 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"; @@ -4314,7 +5544,13 @@ "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."; @@ -4326,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 la forme d'un lien ou d'un code QR - tout le monde peut l'utiliser pour vous contacter."; +"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."; @@ -4337,7 +5573,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Vous pouvez utiliser le format markdown pour mettre en forme les messages :"; -/* No comment provided by engineer. */ +/* 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. */ @@ -4356,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 !"; @@ -4367,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."; @@ -4385,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."; @@ -4430,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é."; @@ -4445,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"; @@ -4457,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(%@)."; @@ -4472,6 +5717,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Vos contacts resteront connectés."; +/* No comment provided by engineer. */ +"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."; @@ -4494,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."; @@ -4503,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"; @@ -4514,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 index b590785606..5a9b6b4e38 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.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)" = " (másolható)"; @@ -23,29 +8,17 @@ "- 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- valamivel jobb csoportok.\n- és még sok más!"; +"- 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!" = "- opcionális értesítés a törölt kapcsolatokról.\n- profilnevek szóközökkel.\n- és még sok más!"; +"- 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." = "- hangüzenetek legfeljebb 5 perces időtartamig.\n- egyedi eltűnési időhatár megadása.\n- előzmények szerkesztése."; - -/* No comment provided by engineer. */ -", " = ", "; - -/* 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. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(új)"; @@ -53,25 +26,19 @@ "(this device v%@)" = "(ez az eszköz: v%@)"; /* No comment provided by engineer. */ -")" = ")"; - -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Hozzájárulás](https://github.com/simplex-chat/simplex-chat#contribute)"; +"[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)" = "[Csillag a GitHubon](https://github.com/simplex-chat/simplex-chat)"; +"[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. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; +"**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. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása a kapcsolattartóhoz."; - -/* No comment provided by engineer. */ -"**Create group**: to create a new group." = "**Csoport létrehozása**: új csoport létrehozásához."; +"**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"; @@ -80,25 +47,28 @@ "**e2e encrypted** video call" = "**e2e titkosított** videóhívás"; /* 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." = "**Privátabb**: 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken megosztásra kerül a SimpleX Chat kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van."; +"**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 notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Legprivátabb**: ne használja a SimpleX Chat értesítési szervert, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**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 kapcsolataiból érkező üzenetek visszafejtését."; +"**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." = "**Figyelem**: NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt."; +"**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 notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Javasolt**: az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési szerverre, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik."; +"**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. */ -"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés**: Az azonnali push-értesítésekhez a kulcstárolóban tárolt jelmondat megadása szükséges."; +"**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**: the archive will be removed." = "**Figyelem**: az archívum törlésre kerül."; +"**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*"; @@ -110,10 +80,10 @@ "## History" = "## Előzmények"; /* copied message info */ -"## In reply to" = "## Válaszul erre:"; +"## In reply to" = "## Válaszul erre"; /* No comment provided by engineer. */ -"#secret#" = "#titkos#"; +"#secret#" = "#titok#"; /* No comment provided by engineer. */ "%@" = "%@"; @@ -137,7 +107,7 @@ "%@ and %@ connected" = "%@ és %@ kapcsolódott"; /* copied message info, at

Hi!

\n

Connect to me via SimpleX Chat

" = "

Üdvözlöm!

\n

Csatlakozzon hozzám a SimpleX Chaten

"; +"

Hi!

\n

Connect to me via SimpleX Chat

" = "

Üdvözlöm!

\n

Csatlakozzon hozzám a SimpleX Chaten keresztül

"; /* No comment provided by engineer. */ "~strike~" = "\\~áthúzott~"; @@ -277,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 nap"; /* time interval */ @@ -286,12 +278,23 @@ /* No comment provided by engineer. */ "1 minute" = "1 perc"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 hónap"; -/* time interval */ +/* 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"; @@ -302,34 +305,31 @@ "30 seconds" = "30 másodperc"; /* No comment provided by engineer. */ -"A few more things" = "Még néhány dolog"; +"A few more things" = "Néhány további dolog"; /* notification title */ -"A new contact" = "Egy új ismerős"; +"A new contact" = "Egy új partner"; /* No comment provided by engineer. */ -"A new random profile will be shared." = "Egy új, véletlenszerű profil kerül megosztásra."; +"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**." = "A rendszer külön TCP-kapcsolatot fog használni **az alkalmazásban található minden csevegési profilhoz**."; +"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." = "A rendszer külön TCP-kapcsolatot fog használni **minden ismerőshöz és csoporttaghoz**.\n**Figyelem**: sok kapcsolódás esetén, az akkumulátor- és adatforgalom fogyasztás jelentősen megnőhet, és egyes kapcsolatok meghiúsulhatnak."; +"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ímváltoztatás megszakítása"; +"Abort changing address" = "Cím módosításának megszakítása"; /* No comment provided by engineer. */ -"Abort changing address?" = "Címváltoztatás megszakítása??"; +"Abort changing address?" = "Megszakítja a cím módosítását?"; /* No comment provided by engineer. */ -"About SimpleX" = "A SimpleX névjegye"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "A SimpleX azonosítóról"; +"About operators" = "Az üzemeltetőkről"; /* No comment provided by engineer. */ "About SimpleX Chat" = "A SimpleX Chat névjegye"; @@ -338,164 +338,253 @@ "above, then choose:" = "gombra fent, majd válassza ki:"; /* No comment provided by engineer. */ -"Accent color" = "Kiemelő szín"; +"Accent" = "Kiemelőszín"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Elfogadás"; /* No comment provided by engineer. */ -"Accept connection request?" = "Kapcsolatfelvétel elfogadása?"; +"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 %@ kapcsolat kérését?"; +"Accept contact request from %@?" = "Elfogadja %@ meghívási kérését?"; -/* accept contact request via notification */ -"Accept incognito" = "Fogadás inkognítóban"; +/* accept contact request via notification +swipe action */ +"Accept incognito" = "Elfogadás inkognitóban"; /* call status */ -"accepted call" = "elfogadott hívás"; +"accepted call" = "fogadott hívás"; /* 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." = "Azonosító hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára."; +"Accepted conditions" = "Elfogadott feltételek"; + +/* chat list item title */ +"accepted invitation" = "elfogadott meghívó"; /* No comment provided by engineer. */ -"Add contact" = "Ismerős hozzáadása"; +"Acknowledged" = "Visszaigazolt"; /* No comment provided by engineer. */ -"Add preset servers" = "Előre beállított kiszolgálók hozzáadása"; +"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…"; +"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 welcome message" = "Üdvözlő üzenet hozzáadása"; +"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 megszakad. A régi fogadási cím kerül felhasználásra."; +"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" = "admin"; +"admin" = "adminisztrátor"; /* feature role */ -"admins" = "adminok"; +"admins" = "adminisztrátorok"; /* No comment provided by engineer. */ -"Admins can block a member for all." = "Az adminok egy tagot mindenki számára letilthatnak."; +"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 adminok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz."; +"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 jóváhagyása %@ számára…"; +"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 app data is deleted." = "Minden alkalmazásadat törölve."; +"All" = "Összes"; /* No comment provided by engineer. */ -"All chats and messages will be deleted - this cannot be undone!" = "Minden csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza!"; +"All app data is deleted." = "Az összes alkalmazásadat törölve."; /* No comment provided by engineer. */ -"All data is erased when it is entered." = "A jelkód megadása után minden adat törlésre kerül."; +"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 group members will remain connected." = "Minden csoporttag kapcsolódva marad."; +"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" = "minden tag"; +"all members" = "összes tag"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone!" = "Minden üzenet törlésre kerül – ez a művelet nem vonható vissza!"; +"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! The messages will be deleted ONLY for you." = "Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az ön számára törlődnek."; +"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 new messages from %@ will be hidden!" = "Minden új üzenet elrejtésre kerül tőle: %@!"; +"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 your contacts will remain connected." = "Minden ismerős kapcsolódva marad."; +"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 your contacts will remain connected. Profile update will be sent to your contacts." = "Ismerőseivel kapcsolatban marad. A profil változtatások frissítésre kerülnek az ismerősöknél."; +"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 your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP átjátszókra."; +"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." = "Hívások engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi."; +"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 disappearing messages only if your contact allows it to you." = "Eltűnő üzenetek engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi az ön számára."; +"Allow calls?" = "Engedélyezi a hívásokat?"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Üzenet végleges törlésének engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. (24 óra)"; +"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 message reactions only if your contact allows them." = "Üzenetreakciók engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi."; +"Allow downgrade" = "Visszafejlesztés engedélyezése"; /* No comment provided by engineer. */ -"Allow message reactions." = "Üzenetreakciók engedélyezése."; +"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 sending direct messages to members." = "Közvetlen üzenetek küldésének engedélyezése a tagok számára."; +"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 sending disappearing messages." = "Eltűnő üzenetek küldésének engedélyezése."; +"Allow message reactions." = "A reakciók hozzáadása az üzenetekhez engedélyezve van."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages. (24 hours)" = "Elküldött üzenetek végleges törlésének engedélyezése. (24 óra)"; +"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 to send files and media." = "Fájlok és médiatartalom küldésének engedélyezése."; +"Allow sending disappearing messages." = "Az eltűnő üzenetek 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."; +"Allow sharing" = "Megosztás engedélyezése"; /* No comment provided by engineer. */ -"Allow to send voice messages." = "Hangüzenetek küldésének engedélyezése."; +"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 voice messages only if your contact allows them." = "Hangüzenetek küldésének engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi."; +"Allow to report messsages to moderators." = "Az üzenetek jelentése a moderátorok felé engedélyezve van."; /* No comment provided by engineer. */ -"Allow voice messages?" = "Hangüzenetek engedélyezése?"; +"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 your contacts adding message reactions." = "Ismerősök általi üzenetreakciók hozzáadásának engedélyezése."; +"Allow to send SimpleX links." = "A SimpleX-hivatkozások küldése engedélyezve van."; /* No comment provided by engineer. */ -"Allow your contacts to call you." = "Hívások engedélyezése ismerősök számára."; +"Allow to send voice messages." = "A hangüzenetek küldése engedélyezve van."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Elküldött üzenetek végleges törlésének engedélyezése az ismerősök számára. (24 óra)"; +"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 your contacts to send disappearing messages." = "Eltűnő üzenetek engedélyezése ismerősök számára."; +"Allow voice messages?" = "Engedélyezi a hangüzeneteket?"; /* No comment provided by engineer. */ -"Allow your contacts to send voice messages." = "Hangüzenetek küldésének engedélyezése ismerősök számára."; +"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?"; @@ -510,19 +599,28 @@ "always" = "mindig"; /* No comment provided by engineer. */ -"Always use relay" = "Mindig használjon átjátszó kiszolgálót"; +"Always use private routing." = "Mindig használjon privát útválasztást."; /* 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 jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik."; +"Always use relay" = "Mindig használjon továbbítókiszolgálót"; /* No comment provided by engineer. */ -"and %lld other events" = "és %lld további esemény"; +"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. */ -"App build: %@" = "Az alkalmazás build száma: %@"; +"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"; @@ -531,19 +629,25 @@ "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 icon" = "Alkalmazás ikon"; +"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ítésre kerül egy önmegsemmisítő jelkóddal."; +"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 version" = "Alkalmazás verzió"; +"App session" = "Alkalmazás munkamenete"; /* No comment provided by engineer. */ -"App version: v%@" = "Alkalmazás verzió: v%@"; +"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"; @@ -551,14 +655,47 @@ /* 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" = "Csatolás"; +"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"; @@ -570,13 +707,13 @@ "audio call (not e2e encrypted)" = "hanghívás (nem e2e titkosított)"; /* chat feature */ -"Audio/video calls" = "Hang-/videóhívások"; +"Audio/video calls" = "Hang- és videóhívások"; /* No comment provided by engineer. */ -"Audio/video calls are prohibited." = "A hang- és videóhívások le vannak tiltva."; +"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 megszakítva"; +"Authentication cancelled" = "Hitelesítés visszavonva"; /* No comment provided by engineer. */ "Authentication failed" = "Sikertelen hitelesítés"; @@ -594,22 +731,28 @@ "Auto-accept" = "Automatikus elfogadás"; /* No comment provided by engineer. */ -"Auto-accept contact requests" = "Ismerős jelölések automatikus elfogadása"; +"Auto-accept contact requests" = "Meghívási kérések automatikus elfogadása"; /* No comment provided by engineer. */ -"Auto-accept images" = "Fotók automatikus elfogadása"; +"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. */ -"Bad desktop address" = "Hibás számítógép azonosító"; - -/* integrity error chat item */ -"bad message hash" = "téves üzenet hash"; +"Background" = "Háttér"; /* No comment provided by engineer. */ -"Bad message hash" = "Téves üzenet hash"; +"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"; @@ -618,52 +761,86 @@ "Bad message ID" = "Téves üzenet ID"; /* No comment provided by engineer. */ -"Better groups" = "Javított csoportok"; +"Better calls" = "Továbbfejlesztett hívásélmény"; /* No comment provided by engineer. */ -"Better messages" = "Jobb üzenetek"; +"Better groups" = "Továbbfejlesztett csoportok"; /* No comment provided by engineer. */ -"Block" = "Blokkolás"; +"Better groups performance" = "Továbbfejlesztett, gyorsabb csoportok"; /* No comment provided by engineer. */ -"Block for all" = "Mindenki számára letiltva"; +"Better message dates." = "Továbbfejlesztett üzenetdátumok."; /* No comment provided by engineer. */ -"Block group members" = "Csoporttagok blokkolása"; +"Better messages" = "Továbbfejlesztett üzenetek"; /* No comment provided by engineer. */ -"Block member" = "Tag blokkolása"; +"Better networking" = "Jobb hálózatkezelés"; /* No comment provided by engineer. */ -"Block member for all?" = "Tag letiltása mindenki számára?"; +"Better notifications" = "Továbbfejlesztett értesítések"; /* No comment provided by engineer. */ -"Block member?" = "Tag blokkolása?"; +"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" = "blokkolva"; +"blocked" = "letiltva"; /* rcv group event chat item */ -"blocked %@" = "%@ letiltva"; +"blocked %@" = "letiltotta őt: %@"; -/* marked deleted chat item preview text */ -"blocked by admin" = "letiltva az admin által"; +/* 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 admin által"; +"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 is hozzáadhat üzenetreakciókat."; +"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 törölheti véglegesen az elküldött üzeneteket. (24 óra)"; +"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 indítani."; +"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."; @@ -675,13 +852,28 @@ "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. */ -"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Csevegési profil (alapértelmezett) vagy [kapcsolat alapján] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA)."; +"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" = "hiba a hívásban"; +"call error" = "híváshiba"; /* call status */ "call in progress" = "hívás folyamatban"; @@ -693,80 +885,114 @@ "Calls" = "Hívások"; /* No comment provided by engineer. */ -"Camera not available" = "A fényképező nem elérhető"; +"Calls prohibited!" = "A hívások le vannak tiltva!"; /* No comment provided by engineer. */ -"Can't invite contact!" = "Ismerősök meghívása le van tiltva!"; +"Camera not available" = "A kamera nem elérhető"; /* No comment provided by engineer. */ -"Can't invite contacts!" = "Ismerősök meghívása nem lehetséges!"; +"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 %@" = "%@ törölve"; +"cancelled %@" = "%@ visszavonva"; /* 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"; +"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" = "Változtatás"; +"Change" = "Módosítás"; -/* No comment provided by engineer. */ -"Change database passphrase?" = "Adatbázis jelmondat megváltoztatása?"; +/* alert title */ +"Change automatic message deletion?" = "Módosítja az automatikus üzenettörlést?"; /* authentication reason */ -"Change lock mode" = "Zárolási mód megváltoztatása"; +"Change chat profiles" = "Csevegési profilok módosítása"; /* No comment provided by engineer. */ -"Change member role?" = "Tag szerepkörének megváltoztatása?"; +"Change database passphrase?" = "Módosítja az adatbázis jelmondatát?"; /* authentication reason */ -"Change passcode" = "Jelkód megváltoztatása"; +"Change lock mode" = "Zárolási mód módosítása"; /* No comment provided by engineer. */ -"Change receiving address" = "A fogadó cím megváltoztatása"; - -/* No comment provided by engineer. */ -"Change receiving address?" = "Megváltoztatja a fogadó címet?"; - -/* No comment provided by engineer. */ -"Change role" = "Szerepkör megváltoztatása"; +"Change member role?" = "Módosítja a tag szerepkörét?"; /* authentication reason */ -"Change self-destruct mode" = "Önmegsemmisítő mód megváltoztatása"; +"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 megváltoztatása"; +set passcode view */ +"Change self-destruct passcode" = "Önmegsemmisítő-jelkód módosítása"; /* chat item text */ -"changed address for you" = "cím megváltoztatva"; +"changed address for you" = "módosította a címet az Ön számára"; /* rcv group event chat item */ -"changed role of %@ to %@" = "%1$@ szerepkörét megváltoztatta erre: %2$@"; +"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 %@" = "megváltoztatta a szerepkörét erre: %@"; +"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…" = "azonosító megváltoztatása…"; +"changing address…" = "cím módosítása…"; /* No comment provided by engineer. */ -"Chat archive" = "Csevegési archívum"; +"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"; @@ -777,6 +1003,9 @@ /* 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"; @@ -784,10 +1013,13 @@ "Chat is running" = "A csevegés fut"; /* No comment provided by engineer. */ -"Chat is stopped" = "A csevegés leállt"; +"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 leá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 megkezdése előtt."; +"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!"; @@ -795,17 +1027,38 @@ /* 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 szkennelje be a QR-kódot."; +"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"; @@ -814,43 +1067,97 @@ "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" = "Beszélgetés kiürítése"; +"Clear conversation" = "Üzenetek kiürítése"; /* No comment provided by engineer. */ -"Clear conversation?" = "Beszélgetés kiürítése?"; +"Clear conversation?" = "Kiüríti az üzeneteket?"; /* No comment provided by engineer. */ -"Clear private notes?" = "Privát jegyzetek törlése?"; +"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. */ -"colored" = "színes"; +"Color chats with the new themes." = "Csevegések színezése új témákkal."; /* No comment provided by engineer. */ -"Colors" = "Színek"; +"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"; +"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 az ismerősökkel."; +"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. */ -"Configure ICE servers" = "ICE kiszolgálók beállítása"; +"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 database upgrades" = "Adatbázis frissítés megerősítése"; +"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"; @@ -865,11 +1172,14 @@ "Confirm password" = "Jelszó megerősítése"; /* No comment provided by engineer. */ -"Confirm that you remember database passphrase to migrate it." = "Erősítse meg, hogy emlékszik az adatbázis jelmondatára az átköltöztetéshez."; +"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"; @@ -880,40 +1190,49 @@ "Connect incognito" = "Kapcsolódás inkognitóban"; /* No comment provided by engineer. */ -"Connect to desktop" = "Kapcsolódás számítógéphez"; +"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."; +"connect to SimpleX Chat developers." = "kapcsolódás a SimpleX Chat fejlesztőkhöz."; /* No comment provided by engineer. */ -"Connect to yourself?" = "Kapcsolódás saját magához?"; +"Connect to your friends faster." = "Kapcsolódjon gyorsabban a partnereihez."; /* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az egyszer használatos hivatkozása!"; +"Connect to yourself?" = "Kapcsolódik saját magához?"; /* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódás saját magához?\nEz a SimpleX azonosítója!"; +"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 via contact address" = "Kapcsolódás a kapcsolattartási azonosítón keresztül"; +"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álatos hivatkozáson keresztül"; +"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 ezzel: %@"; +"Connect with %@" = "Kapcsolódás a következővel: %@"; /* No comment provided by engineer. */ -"connected" = "kapcsolódva"; +"connected" = "kapcsolódott"; /* No comment provided by engineer. */ -"Connected desktop" = "Csatlakoztatott számítógép"; +"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ódva"; +"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"; @@ -921,6 +1240,9 @@ /* 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)"; @@ -928,29 +1250,38 @@ "connecting (announced)" = "kapcsolódás (bejelentve)"; /* No comment provided by engineer. */ -"connecting (introduced)" = "kapcsolódás (bejelentve)"; +"connecting (introduced)" = "kapcsolódás (bemutatkozva)"; /* No comment provided by engineer. */ "connecting (introduction invitation)" = "kapcsolódás (bemutatkozó meghívó)"; /* call status */ -"connecting call" = "hívás kapcsolódik…"; +"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: %@)"; +"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"; -/* chat list item title */ +/* 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"; @@ -961,73 +1292,112 @@ "connection established" = "kapcsolat létrehozva"; /* No comment provided by engineer. */ -"Connection request sent!" = "Kapcsolódási kérés elküldve!"; +"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" = "Kapcsolat időtúllépés"; +"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$@ megváltoztatta a nevét erre: %2$@"; +"contact %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; /* No comment provided by engineer. */ -"Contact allows" = "Ismerős engedélyezi"; +"Contact allows" = "Partner engedélyezi"; /* No comment provided by engineer. */ -"Contact already exists" = "Létező ismerős"; +"Contact already exists" = "A partner már létezik"; /* No comment provided by engineer. */ -"contact has e2e encryption" = "az ismerősnél az e2e titkosítás elérhető"; +"Contact deleted!" = "Partner törölve!"; /* No comment provided by engineer. */ -"contact has no e2e encryption" = "az ismerősnél az e2e titkosítás nem elérhető"; +"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:" = "Ismerős elrejtve:"; +"Contact hidden:" = "Rejtett név:"; /* notification */ -"Contact is connected" = "Ismerőse kapcsolódott"; +"Contact is connected" = "Partnere kapcsolódott"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Az ismerőse még nem kapcsolódott!"; +"Contact is deleted." = "Törölt partner."; /* No comment provided by engineer. */ -"Contact name" = "Ismerős neve"; +"Contact name" = "Csak név"; /* No comment provided by engineer. */ -"Contact preferences" = "Ismerős beállításai"; +"Contact preferences" = "Partnerbeállítások"; /* No comment provided by engineer. */ -"Contacts" = "Ismerősök"; +"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 can mark messages for deletion; you will be able to view them." = "Az ismerősök törlésre jelölhetnek üzeneteket ; megtekintheti őket."; +"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"; -/* chat item action */ +/* 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. */ -"Core version: v%@" = "Alapverziószám: v%@"; +"Copy error" = "Másolási hiba"; /* No comment provided by engineer. */ -"Correct name to %@?" = "Név javítása erre: %@?"; +"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 a group using a random profile." = "Csoport létrehozása véletlenszerűen létrehozott profillal."; +"Create 1-time link" = "Egyszer használható meghívó létrehozása"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Azonosító létrehozása, hogy az emberek kapcsolatba léphessenek önnel."; +"Create a group using a random profile." = "Csoport létrehozása véletlenszerű profillal."; /* server test step */ "Create file" = "Fájl létrehozása"; @@ -1036,46 +1406,52 @@ "Create group" = "Csoport létrehozása"; /* No comment provided by engineer. */ -"Create group link" = "Csoportos hivatkozás létrehozása"; +"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 new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Új profil létrehozása az [asztali kliensben](https://simplex.chat/downloads/). 💻"; +"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" = "Várólista létrehozása"; +"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 azonosító létrehozása"; +"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 at" = "Létrehozva ekkor:"; +"Created" = "Létrehozva"; + +/* No comment provided by engineer. */ +"Created at" = "Létrehozva"; /* copied message info */ -"Created at: %@" = "Létrehozva ekkor: %@"; +"Created at: %@" = "Létrehozva: %@"; /* No comment provided by engineer. */ -"Created on %@" = "Létrehozva %@"; - -/* No comment provided by engineer. */ -"Creating archive link" = "Archív hivatkozás létrehozása"; +"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" = "szerző"; +"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"; @@ -1084,84 +1460,103 @@ "Current passphrase…" = "Jelenlegi jelmondat…"; /* No comment provided by engineer. */ -"Currently maximum supported file size is %@." = "Jelenleg a maximális támogatott fájlméret %@."; - -/* dropdown time picker choice */ -"custom" = "egyedi"; +"Current profile" = "Jelenlegi profil"; /* No comment provided by engineer. */ -"Custom time" = "Személyreszabott idő"; +"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. */ -"Database downgrade" = "Visszatérés a korábbi adatbázis verzióra"; +"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ül és tárolódik a kulcstárolóban.\n"; +"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" = "Adatbázis titkosítási jelmondat frissítve lesz.\n"; +"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ázis hiba"; +"Database error" = "Adatbázishiba"; /* No comment provided by engineer. */ -"Database ID" = "Adatbázis ID"; +"Database ID" = "Adatbázis-azonosító"; /* copied message info */ -"Database ID: %d" = "Adatbázis azonosító: %d"; +"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 átviteli izolációs beállítások."; +"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, megváltoztatható."; +"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őtti módosítás szükséges."; +"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"; +"Database passphrase" = "Adatbázis-jelmondat"; /* No comment provided by engineer. */ -"Database passphrase & export" = "Adatbázis jelmondat és exportálás"; +"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 eltér a kulcstárlóban mentettől."; +"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." = "Adatbázis jelmondat szükséges a csevegés megnyitásához."; +"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 nincs visszafelé átköltöztetés ehhez: %@"; +"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 kulcstárolóban lesz tárolva.\n"; +"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ításra kerül.\n"; +"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 migrálásra kerül"; +"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"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "visszafejtési hibák"; + +/* delete after time +pref value */ "default (%@)" = "alapértelmezett (%@)"; /* No comment provided by engineer. */ @@ -1170,50 +1565,54 @@ /* No comment provided by engineer. */ "default (yes)" = "alapértelmezett (igen)"; -/* chat item action */ +/* 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" = "Azonosító törlése"; +"Delete address" = "Cím törlése"; /* No comment provided by engineer. */ -"Delete address?" = "Azonosító törlése?"; +"Delete address?" = "Törli a címet?"; /* No comment provided by engineer. */ -"Delete after" = "Törlés miután"; +"Delete after" = "Törlés ennyi idő után"; /* No comment provided by engineer. */ -"Delete all files" = "Minden fájl törlése"; +"Delete all files" = "Az összes fájl törlése"; /* No comment provided by engineer. */ -"Delete and notify contact" = "Törlés és ismerős értesítése"; +"Delete and notify contact" = "Törlés, és a partner értesítése"; /* No comment provided by engineer. */ -"Delete archive" = "Archívum törlése"; +"Delete chat" = "Csevegés törlése"; /* No comment provided by engineer. */ -"Delete chat archive?" = "Csevegési archívum törlése?"; +"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?" = "Csevegési profil törlése?"; +"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" = "Ismerős törlése"; +"Delete contact" = "Partner törlése"; /* No comment provided by engineer. */ -"Delete Contact" = "Ismerős törlése"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Ismerős törlése?\nEz a művelet nem vonható vissza!"; +"Delete contact?" = "Törli a partnert?"; /* No comment provided by engineer. */ "Delete database" = "Adatbázis törlése"; @@ -1225,104 +1624,143 @@ "Delete file" = "Fájl törlése"; /* No comment provided by engineer. */ -"Delete files and media?" = "Fájlok és a médiatartalmak törlése?"; +"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 minden csevegési profilból"; +"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 mindenkinél"; +"Delete for everyone" = "Törlés az összes tagnál"; /* No comment provided by engineer. */ -"Delete for me" = "Törlés nálam"; +"Delete for me" = "Csak nálam"; /* No comment provided by engineer. */ "Delete group" = "Csoport törlése"; /* No comment provided by engineer. */ -"Delete group?" = "Csoport törlése?"; +"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" = "Hivatkozás törlése"; +"Delete link" = "Törlés"; /* No comment provided by engineer. */ -"Delete link?" = "Hivatkozás törlése?"; +"Delete link?" = "Törli a hivatkozást?"; + +/* alert title */ +"Delete list?" = "Törli a listát?"; /* No comment provided by engineer. */ -"Delete member message?" = "Csoporttag üzenet törlése?"; +"Delete member message?" = "Törli a tag üzenetét?"; /* No comment provided by engineer. */ -"Delete message?" = "Üzenet törlése?"; +"Delete message?" = "Törli az üzenetet?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Üzenetek törlése"; /* No comment provided by engineer. */ -"Delete messages after" = "Üzenetek törlése miután"; +"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?" = "Régi adatbázis törlése?"; +"Delete old database?" = "Törli a régi adatbázist?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Függőben lévő kapcsolat törlése"; +"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?" = "Függő kapcsolatfelvételi kérések törlése?"; +"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" = "Várólista törlése"; +"Delete queue" = "Sorba állítás törlése"; /* No comment provided by engineer. */ -"Delete user profile?" = "Felhasználói profil törlése?"; +"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 at" = "Törölve ekkor:"; +"Deleted" = "Törölve"; + +/* No comment provided by engineer. */ +"Deleted at" = "Törölve"; /* copied message info */ -"Deleted at: %@" = "Törölve ekkor: %@"; +"Deleted at: %@" = "Törölve: %@"; /* rcv direct event chat item */ -"deleted contact" = "törölt ismerős"; +"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!" = "Kézbesítési igazolások kikapcsolva!"; +"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 igazolások!"; +"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 azonosítója"; +"Desktop address" = "Számítógép címe"; /* No comment provided by engineer. */ -"Desktop app version %@ is not compatible with this app." = "Az asztali kliens verziója %@ nem kompatibilis ezzel az alkalmazással."; +"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"; @@ -1330,16 +1768,16 @@ "Device" = "Eszköz"; /* No comment provided by engineer. */ -"Device authentication is disabled. Turning off SimpleX Lock." = "Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása."; +"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." = "Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került."; +"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ések az alkalmazásban/adatbázisban: %@ / %@"; +"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, avatarok és átviteli izoláció."; +"Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitel-izoláció."; /* connection level description */ "direct" = "közvetlen"; @@ -1348,20 +1786,32 @@ "Direct messages" = "Közvetlen üzenetek"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Ebben a csoportban tiltott a tagok közötti közvetlen üzenetek küldése."; +"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 mindenki számára"; +"Disable for all" = "Letiltás"; /* authentication reason */ -"Disable SimpleX Lock" = "SimpleX zárolás kikapcsolása"; +"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"; @@ -1369,59 +1819,90 @@ "Disappearing messages" = "Eltűnő üzenetek"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this chat." = "Az eltűnő üzenetek le vannak tiltva ebben a csevegésben."; +"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 in this group." = "Az eltűnő üzenetek küldése le van tiltva ebben a csoportban."; +"Disappearing messages are prohibited." = "Az eltűnő üzenetek küldése le van tiltva."; /* No comment provided by engineer. */ -"Disappears at" = "Eltűnik ekkor:"; +"Disappears at" = "Eltűnik"; /* copied message info */ -"Disappears at: %@" = "Eltűnik ekkor: %@"; +"Disappears at: %@" = "Eltűnik: %@"; /* server test step */ "Disconnect" = "Kapcsolat bontása"; /* No comment provided by engineer. */ -"Disconnect desktop?" = "Számítógép leválasztása?"; +"Disconnect desktop?" = "Leválasztja a számítógépet?"; /* No comment provided by engineer. */ -"Discover and join groups" = "Helyi csoportok felfedezése és csatlakozás"; +"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" = "Későbbre halaszt"; +"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 kerüljenek elküldésre az új tagok számára."; +"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 use SimpleX for emergency calls." = "NE használja a SimpleX-et segélyhívásokhoz."; +"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. */ -"Don't create address" = "Ne hozzon létre azonosítót"; +"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. */ -"Downgrade and open chat" = "Visszatérés a korábbi verzióra és a csevegés megnyitása"; +"Done" = "Kész"; -/* chat item action */ +/* 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"; @@ -1429,10 +1910,13 @@ "Downloading link details" = "Letöltési hivatkozás részletei"; /* No comment provided by engineer. */ -"Duplicate display name!" = "Duplikált megjelenítési név!"; +"Duplicate display name!" = "Duplikált megjelenítendő név!"; /* integrity error chat item */ -"duplicate message" = "duplikálódott üzenet"; +"duplicate message" = "duplikált üzenet"; + +/* No comment provided by engineer. */ +"duplicates" = "duplikációk"; /* No comment provided by engineer. */ "Duration" = "Időtartam"; @@ -1440,11 +1924,14 @@ /* 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" = "A csoport profiljának szerkesztése"; +"Edit group profile" = "Csoportprofil szerkesztése"; /* No comment provided by engineer. */ "Enable" = "Engedélyezés"; @@ -1452,20 +1939,23 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Engedélyezés (felülírások megtartásával)"; -/* No comment provided by engineer. */ -"Enable automatic message deletion?" = "Automatikus üzenet törlés engedélyezése?"; +/* 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 for all" = "Engedélyezés mindenki számára"; +"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?" = "Azonnali értesítések engedélyezése?"; +"Enable instant notifications?" = "Engedélyezi az azonnali értesítéseket?"; /* No comment provided by engineer. */ "Enable lock" = "Zárolás engedélyezése"; @@ -1474,43 +1964,46 @@ "Enable notifications" = "Értesítések engedélyezése"; /* No comment provided by engineer. */ -"Enable periodic notifications?" = "Időszakos értesítések engedélyezése?"; +"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"; +"Enable self-destruct passcode" = "Önmegsemmisítő-jelkód engedélyezése"; /* authentication reason */ -"Enable SimpleX Lock" = "SimpleX zárolás engedélyezése"; +"Enable SimpleX Lock" = "SimpleX-zár bekapcsolása"; /* No comment provided by engineer. */ -"Enable TCP keep-alive" = "TCP életben tartásának engedélyezése"; +"Enable TCP keep-alive" = "TCP életben tartása"; /* enabled status */ "enabled" = "engedélyezve"; /* No comment provided by engineer. */ -"Enabled for" = "Engedélyezve"; +"Enabled" = "Engedélyezve"; + +/* No comment provided by engineer. */ +"Enabled for" = "Számukra engedélyezve"; /* enabled status */ -"enabled for contact" = "engedélyezve az ismerős számára"; +"enabled for contact" = "engedélyezve a partner számára"; /* enabled status */ -"enabled for you" = "engedélyezve az ön számára"; +"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?" = "Adatbázis titkosítása?"; +"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" = "Tárolt fájlok és médiatartalmak titkosítása"; +"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"; @@ -1519,16 +2012,16 @@ "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 leállt"; +"Encrypted message: app is stopped" = "Titkosított üzenet: az alkalmazás megállt"; /* notification */ -"Encrypted message: database error" = "Titkosított üzenet: adatbázis hiba"; +"Encrypted message: database error" = "Titkosított üzenet: adatbázishiba"; /* notification */ -"Encrypted message: database migration error" = "Titkosított üzenet: adatbázis-átköltöztetés hiba"; +"Encrypted message: database migration error" = "Titkosított üzenet: adatbázis-átköltöztetési hiba"; /* notification */ -"Encrypted message: keychain error" = "Titkosított üzenet: kulcstároló hiba"; +"Encrypted message: keychain error" = "Titkosított üzenet: kulcstartó hiba"; /* notification */ "Encrypted message: no passphrase" = "Titkosított üzenet: nincs jelmondat"; @@ -1537,7 +2030,7 @@ "Encrypted message: unexpected error" = "Titkosított üzenet: váratlan hiba"; /* chat item text */ -"encryption agreed" = "titkosítás egyeztetve"; +"encryption agreed" = "titkosítás elfogadva"; /* chat item text */ "encryption agreed for %@" = "titkosítás elfogadva %@ számára"; @@ -1546,25 +2039,28 @@ "encryption ok" = "titkosítás rendben"; /* chat item text */ -"encryption ok for %@" = "titkosítás rendben vele: %@"; +"encryption ok for %@" = "titkosítás rendben %@ számára"; /* chat item text */ -"encryption re-negotiation allowed" = "titkosítás újraegyeztetés engedélyezve"; +"encryption re-negotiation allowed" = "a titkosítás újraegyeztetése engedélyezve van"; /* chat item text */ -"encryption re-negotiation allowed for %@" = "titkosítás újraegyeztetés engedélyezve vele: %@"; +"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" = "Titkosítás újraegyeztetési hiba"; +"Encryption re-negotiation error" = "Hiba történt a titkosítás újraegyeztetésekor"; /* No comment provided by engineer. */ -"Encryption re-negotiation failed." = "Sikertelen titkosítás-újraegyeztetés."; +"Encryption re-negotiation failed." = "Nem sikerült a titkosítást újraegyeztetni."; /* chat item text */ -"encryption re-negotiation required" = "titkosítás újraegyeztetés szükséges"; +"encryption re-negotiation required" = "a titkosítás újraegyeztetése szükséges"; /* chat item text */ -"encryption re-negotiation required for %@" = "titkosítás újraegyeztetés szükséges %@ számára"; +"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"; @@ -1573,34 +2069,34 @@ "ended call %@" = "%@ hívása befejeződött"; /* No comment provided by engineer. */ -"Enter correct passphrase." = "Helyes jelmondat bevitele."; +"Enter correct passphrase." = "Adja meg a helyes jelmondatot."; /* No comment provided by engineer. */ -"Enter group name…" = "Csoportnév megadása…"; +"Enter group name…" = "Adja meg a csoport nevét…"; /* No comment provided by engineer. */ -"Enter Passcode" = "Jelkód megadása"; +"Enter Passcode" = "Adja meg a jelkódot"; /* No comment provided by engineer. */ -"Enter passphrase" = "Jelmondat megadása"; +"Enter passphrase" = "Adja meg a jelmondatot"; /* No comment provided by engineer. */ -"Enter passphrase…" = "Jelmondat megadása…"; +"Enter passphrase…" = "Adja meg a jelmondatot…"; /* No comment provided by engineer. */ -"Enter password above to show!" = "Jelszó megadása a megjelenítéshez!"; +"Enter password above to show!" = "Adja meg a jelszót fentebb a megjelenítéshez!"; /* No comment provided by engineer. */ -"Enter server manually" = "Kiszolgáló megadása kézzel"; +"Enter server manually" = "Adja meg a kiszolgálót kézzel"; /* No comment provided by engineer. */ -"Enter this device name…" = "Eszköznév megadása…"; +"Enter this device name…" = "Adja meg ennek az eszköznek a nevét…"; /* placeholder */ -"Enter welcome message…" = "Üdvözlő üzenetet megadása…"; +"Enter welcome message…" = "Adja meg az üdvözlőüzenetet…"; /* placeholder */ -"Enter welcome message… (optional)" = "Üdvözlő üzenetet megadása… (opcionális)"; +"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…"; @@ -1612,200 +2108,265 @@ "Error" = "Hiba"; /* No comment provided by engineer. */ -"Error aborting address change" = "Hiba az azonosító megváltoztatásának megszakításakor"; +"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 kapcsolatfelvételi kérelem elfogadásakor"; +"Error accepting contact request" = "Hiba történt a meghívási kérés elfogadásakor"; /* No comment provided by engineer. */ -"Error accessing database file" = "Hiba az adatbázisfájl elérésekor"; +"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 adding member(s)" = "Hiba a tag(-ok) hozzáadásakor"; +"Error changing address" = "Hiba történt a cím módosításakor"; /* No comment provided by engineer. */ -"Error changing address" = "Hiba az azonosító megváltoztatásakor"; +"Error changing connection profile" = "Hiba történt a kapcsolati profilra való váltáskor"; /* No comment provided by engineer. */ -"Error changing role" = "Hiba a szerepkör megváltoztatásakor"; +"Error changing role" = "Hiba történt a szerepkör módosításakor"; /* No comment provided by engineer. */ -"Error changing setting" = "Hiba a beállítás megváltoztatásakor"; +"Error changing setting" = "Hiba történt a beállítás módosításakor"; /* No comment provided by engineer. */ -"Error creating address" = "Hiba az azonosító létrehozásakor"; +"Error changing to incognito!" = "Hiba történt az inkognitóprofilra való váltáskor!"; /* No comment provided by engineer. */ -"Error creating group" = "Hiba a csoport létrehozásakor"; +"Error checking token status" = "Hiba történt a token állapotának ellenőrzésekor"; /* No comment provided by engineer. */ -"Error creating group link" = "Hiba a csoport hivatkozásának létrehozásakor"; +"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 member contact" = "Hiba az ismerőssel történő kapcsolat létrehozásában"; +"Error creating address" = "Hiba történt a cím létrehozásakor"; /* No comment provided by engineer. */ -"Error creating message" = "Hiba az üzenet létrehozásakor"; +"Error creating group" = "Hiba történt a csoport létrehozásakor"; /* No comment provided by engineer. */ -"Error creating profile!" = "Hiba a profil létrehozásakor!"; +"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 decrypting file" = "Hiba a fájl visszafejtésekor"; +"Error creating member contact" = "Hiba történt a partnerrel történő kapcsolat létrehozásában"; /* No comment provided by engineer. */ -"Error deleting chat database" = "Hiba a csevegési adatbázis törlésekor"; +"Error creating message" = "Hiba történt az üzenet létrehozásakor"; /* No comment provided by engineer. */ -"Error deleting chat!" = "Hiba a csevegés törlésekor!"; +"Error creating profile!" = "Hiba történt a profil létrehozásakor!"; /* No comment provided by engineer. */ -"Error deleting connection" = "Hiba a kapcsolat törlésekor"; +"Error creating report" = "Hiba történt a jelentés létrehozásakor"; /* No comment provided by engineer. */ -"Error deleting contact" = "Hiba az ismerős törlésekor"; +"Error decrypting file" = "Hiba történt a fájl visszafejtésekor"; /* No comment provided by engineer. */ -"Error deleting database" = "Hiba az adatbázis törlésekor"; +"Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor"; /* No comment provided by engineer. */ -"Error deleting old database" = "Hiba a régi adatbázis törlésekor"; +"Error deleting chat!" = "Hiba történt a csevegés törlésekor!"; /* No comment provided by engineer. */ -"Error deleting token" = "Hiba a token törlésekor"; +"Error deleting connection" = "Hiba történt a kapcsolat törlésekor"; /* No comment provided by engineer. */ -"Error deleting user profile" = "Hiba a felhasználói profil törlésekor"; +"Error deleting database" = "Hiba történt az adatbázis törlésekor"; /* No comment provided by engineer. */ -"Error downloading the archive" = "Hiba az archívum letöltésekor"; +"Error deleting old database" = "Hiba történt a régi adatbázis törlésekor"; /* No comment provided by engineer. */ -"Error enabling delivery receipts!" = "Hiba a kézbesítési jelentések engedélyezésekor!"; +"Error deleting token" = "Hiba történt a token törlésekor"; /* No comment provided by engineer. */ -"Error enabling notifications" = "Hiba az értesítések engedélyezésekor"; +"Error deleting user profile" = "Hiba történt a felhasználó-profil törlésekor"; /* No comment provided by engineer. */ -"Error encrypting database" = "Hiba az adatbázis titkosításakor"; +"Error downloading the archive" = "Hiba történt az archívum letöltésekor"; /* No comment provided by engineer. */ -"Error exporting chat database" = "Hiba a csevegési adatbázis exportálásakor"; +"Error enabling delivery receipts!" = "Hiba történt a kézbesítési jelentések engedélyezésekor!"; /* No comment provided by engineer. */ -"Error importing chat database" = "Hiba a csevegési adatbázis importálásakor"; +"Error enabling notifications" = "Hiba történt az értesítések engedélyezésekor"; /* No comment provided by engineer. */ -"Error joining group" = "Hiba a csoporthoz való csatlakozáskor"; +"Error encrypting database" = "Hiba történt az adatbázis titkosításakor"; /* No comment provided by engineer. */ -"Error loading %@ servers" = "Hiba a %@ kiszolgálók betöltésekor"; +"Error exporting chat database" = "Hiba történt a csevegési adatbázis exportálásakor"; /* No comment provided by engineer. */ -"Error opening chat" = "Hiba a csevegés megnyitásakor"; +"Error exporting theme: %@" = "Hiba történt a téma exportálásakor: %@"; /* No comment provided by engineer. */ -"Error receiving file" = "Hiba a fájl fogadásakor"; +"Error importing chat database" = "Hiba történt a csevegési adatbázis importálásakor"; /* No comment provided by engineer. */ -"Error removing member" = "Hiba a tag eltávolításakor"; +"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 saving %@ servers" = "Hiba történt a %@ kiszolgálók mentése közben"; +"Error migrating settings" = "Hiba történt a beállítások átköltöztetésekor"; /* No comment provided by engineer. */ -"Error saving group profile" = "Hiba a csoport profil mentésekor"; +"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 saving ICE servers" = "Hiba az ICE kiszolgálók mentésekor"; +"Error reconnecting server" = "Hiba történt a kiszolgálóhoz való újrakapcsolódáskor"; /* No comment provided by engineer. */ -"Error saving passcode" = "Hiba a jelkód mentése közben"; +"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 saving passphrase to keychain" = "Hiba a jelmondat kulcstárolóba történő mentésekor"; +"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 a beállítások mentésekor"; +"Error saving settings" = "Hiba történt a beállítások mentésekor"; /* No comment provided by engineer. */ -"Error saving user password" = "Hiba a felhasználó jelszavának mentésekor"; +"Error saving user password" = "Hiba történt a felhasználó jelszavának mentésekor"; /* No comment provided by engineer. */ -"Error scanning code: %@" = "Hiba a kód beolvasása közben: %@"; +"Error scanning code: %@" = "Hiba történt a kód beolvasásakor: %@"; /* No comment provided by engineer. */ -"Error sending email" = "Hiba az e-mail küldésekor"; +"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 az üzenet küldésekor"; +"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 igazolások beállításakor!"; +"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 a csevegés elindításakor"; +"Error starting chat" = "Hiba történt a csevegés elindításakor"; /* No comment provided by engineer. */ -"Error stopping chat" = "Hiba a csevegés megállításakor"; +"Error stopping chat" = "Hiba történt a csevegés megállításakor"; /* No comment provided by engineer. */ -"Error switching profile!" = "Hiba a profil váltásakor!"; +"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 a kapcsolat szinkronizálása során"; +"Error synchronizing connection" = "Hiba történt a kapcsolat szinkronizálásakor"; /* No comment provided by engineer. */ -"Error updating group link" = "Hiba a csoport hivatkozás frissítésekor"; +"Error testing server connection" = "Hiba történt a kiszolgáló kapcsolatának tesztelésekor"; /* No comment provided by engineer. */ -"Error updating message" = "Hiba az üzenet frissítésekor"; +"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 a felhasználói beállítások frissítésekor"; +"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 az archívum feltöltésekor"; +"Error uploading the archive" = "Hiba történt az archívum feltöltésekor"; /* No comment provided by engineer. */ -"Error verifying passphrase:" = "Hiba a jelmondat ellenőrzésekor:"; +"Error verifying passphrase:" = "Hiba történt a jelmondat hitelesítésekor:"; /* No comment provided by engineer. */ "Error: " = "Hiba: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Hiba: %@"; /* No comment provided by engineer. */ -"Error: no database file" = "Hiba: nincs adatbázis fájl"; +"Error: no database file" = "Hiba: nincs adatbázisfájl"; /* No comment provided by engineer. */ -"Error: URL is invalid" = "Hiba: az URL érvénytelen"; +"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. */ -"event happened" = "esemény történt"; - /* 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."; @@ -1813,7 +2374,7 @@ "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…"; +"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"; @@ -1822,13 +2383,43 @@ "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 joining and more reliable messages." = "Gyorsabb csatlakozás és megbízhatóbb üzenet kézbesítés."; +"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. */ -"File will be deleted from servers." = "A fájl törölve lesz a kiszolgálóról."; +"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."; @@ -1840,19 +2431,22 @@ "File: %@" = "Fájl: %@"; /* No comment provided by engineer. */ -"Files & media" = "Fájlok és média"; +"Files" = "Fájlok"; + +/* No comment provided by engineer. */ +"Files & media" = "Fájlok és médiatartalmak"; /* chat feature */ -"Files and media" = "Fájlok és médiatartalom"; +"Files and media" = "Fájlok és médiatartalmak"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "A fájlok- és a médiatartalom küldése le van tiltva ebben a csoportban."; +"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" = "Fájlok és média tartalom küldése le van tiltva"; +"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édiatartalom küldése le van tiltva!"; +"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."; @@ -1882,20 +2476,50 @@ "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" = "Ismerős általi javítás nem támogatott"; +"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"; @@ -1903,7 +2527,25 @@ "Forwarded" = "Továbbított"; /* No comment provided by engineer. */ -"Forwarded from" = "Továbbítva innen:"; +"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"; @@ -1915,23 +2557,29 @@ "Full link" = "Teljes hivatkozás"; /* No comment provided by engineer. */ -"Full name (optional)" = "Teljes név (opcionális)"; +"Full name (optional)" = "Teljes név (nem kötelező)"; /* No comment provided by engineer. */ -"Full name:" = "Teljes név:"; +"Fully decentralized – visible only to members." = "Teljesen decentralizált – csak a tagok számára látható."; /* No comment provided by engineer. */ -"Fully decentralized – visible only to members." = "Teljesen decentralizált - kizárólag tagok számára látható."; +"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. */ -"Fully re-implemented - work in background!" = "Teljesen újra implementálva - háttérben történő működés!"; +"Further reduced battery usage" = "Tovább csökkentett akkumulátor-használat"; /* No comment provided by engineer. */ -"Further reduced battery usage" = "Tovább csökkentett akkumulátor használat"; +"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"; @@ -1948,88 +2596,73 @@ "Group display name" = "A csoport megjelenített neve"; /* No comment provided by engineer. */ -"Group full name (optional)" = "Csoport teljes neve (opcionális)"; +"Group full name (optional)" = "A csoport teljes neve (nem kötelező)"; /* No comment provided by engineer. */ -"Group image" = "Csoportkép"; +"Group image" = "Csoport profilképe"; /* No comment provided by engineer. */ -"Group invitation" = "Csoportos meghívó"; +"Group invitation" = "Csoportmeghívó"; /* No comment provided by engineer. */ -"Group invitation expired" = "A csoport meghívó lejárt"; +"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 csoport meghívó már nem érvényes, a küldője törölte."; +"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" = "Csoport hivatkozás"; +"Group link" = "Csoporthivatkozás"; /* No comment provided by engineer. */ -"Group links" = "Csoport hivatkozások"; - -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Csoporttagok üzenetreakciókat adhatnak hozzá."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "A csoport tagjai küldhetnek eltűnő üzeneteket."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "A csoport tagjai küldhetnek fájlokat és médiatartalmakat."; - -/* No comment provided by engineer. */ -"Group members can send SimpleX links." = "A csoport tagjai küldhetnek SimpleX hivatkozásokat."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "A csoport tagjai küldhetnek hangüzeneteket."; +"Group links" = "Csoporthivatkozások"; /* notification */ "Group message:" = "Csoport üzenet:"; /* No comment provided by engineer. */ -"Group moderation" = "Csoport moderáció"; +"Group moderation" = "Csoport moderálása"; /* No comment provided by engineer. */ -"Group preferences" = "Csoport beállítások"; +"Group preferences" = "Csoportbeállítások"; /* No comment provided by engineer. */ -"Group profile" = "Csoport profil"; +"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" = "csoport profil frissítve"; +"group profile updated" = "csoportprofil frissítve"; /* No comment provided by engineer. */ -"Group welcome message" = "Csoport üdvözlő üzenete"; +"Group welcome message" = "A csoport üdvözlőüzenete"; /* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "Csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza!"; +"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örlésre kerül az ön számára - ez a művelet nem vonható vissza!"; +"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. */ -"Help" = "Segítség"; +"Groups" = "Csoportok"; /* No comment provided by engineer. */ -"Hidden" = "Rejtett"; +"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 profil jelszó"; +"Hidden profile password" = "Rejtett profiljelszó"; /* chat item action */ -"Hide" = "Elrejt"; +"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."; @@ -2038,18 +2671,24 @@ "Hide profile" = "Profil elrejtése"; /* No comment provided by engineer. */ -"Hide:" = "Elrejt:"; +"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 kerülnek elküldésre az új tagok számára."; +"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. */ @@ -2059,10 +2698,10 @@ "How to" = "Hogyan"; /* No comment provided by engineer. */ -"How to use it" = "Hogyan használja"; +"How to use it" = "Használati útmutató"; /* No comment provided by engineer. */ -"How to use your servers" = "Kiszolgálók használata"; +"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"; @@ -2071,19 +2710,19 @@ "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 során, vagy ossza meg a hivatkozást."; +"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 törlődik!"; +"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 az önmegsemmisítő jelkódot megadásra kerül:"; +"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 a ** Csináld később** elemre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis áttelepítése)."; +"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" = "Figyelmen kívül hagyás"; +"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."; @@ -2095,13 +2734,13 @@ "Immediately" = "Azonnal"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Spam és visszaélések elleni védelem"; +"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?" = "Csevegési adatbázis importálása?"; +"Import chat database?" = "Importálja a csevegési adatbázist?"; /* No comment provided by engineer. */ "Import database" = "Adatbázis importálása"; @@ -2109,11 +2748,17 @@ /* 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 message delivery" = "Továbbfejlesztett üzenetküldés"; +"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"; @@ -2125,31 +2770,40 @@ "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álasz neki"; +"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"; +"Incognito groups" = "Inkognitócsoportok"; /* No comment provided by engineer. */ -"Incognito mode" = "Inkognitó mód"; +"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 védi személyes adatait azáltal, hogy minden ismerőshöz új véletlenszerű profilt használ."; +"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 hivatkozáson keresztül"; +"incognito via contact address link" = "inkognitó a kapcsolattartási címhivatkozáson keresztül"; /* chat list item description */ -"incognito via group link" = "inkognitó a csoportos hivatkozáson keresztül"; +"incognito via group link" = "inkognitó a csoporthivatkozáson keresztül"; /* chat list item description */ -"incognito via one-time link" = "inkognitó az egyszer használatos hivatkozáson keresztül"; +"incognito via one-time link" = "inkognitó egy egyszer használható meghívón keresztül"; /* notification */ "Incoming audio call" = "Bejövő hanghívás"; @@ -2161,7 +2815,7 @@ "Incoming video call" = "Bejövő videóhívás"; /* No comment provided by engineer. */ -"Incompatible database version" = "Nem kompatibilis adatbázis verzió"; +"Incompatible database version" = "Nem kompatibilis adatbázis-verzió"; /* No comment provided by engineer. */ "Incompatible version" = "Nem kompatibilis verzió"; @@ -2185,28 +2839,46 @@ "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 push notifications will be hidden!\n" = "Az azonnali push értesítések elrejtésre kerülnek!\n"; +"Instant" = "Azonnali"; /* No comment provided by engineer. */ -"Instantly" = "Azonnal"; +"Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések el lesznek rejtve!\n"; /* No comment provided by engineer. */ -"Interface" = "Felület"; +"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és adat"; +"invalid chat data" = "érvénytelen csevegésadat"; /* No comment provided by engineer. */ -"Invalid connection link" = "Érvénytelen kapcsolati hivatkozás"; +"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ő felhaszálónév!"; +"Invalid display name!" = "Érvénytelen megjelenítendő név!"; /* No comment provided by engineer. */ "Invalid link" = "Érvénytelen hivatkozás"; @@ -2223,7 +2895,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Érvénytelen válasz"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Érvénytelen kiszolgálócím!"; /* item status text */ @@ -2235,50 +2907,62 @@ /* 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ívta"; +"invited" = "meghíva"; /* rcv group event chat item */ -"invited %@" = "meghívta %@-t"; +"invited %@" = "meghívta őt: %@"; /* chat list item title */ -"invited to connect" = "meghívta, hogy csatlakozzon"; +"invited to connect" = "Függőben lévő meghívó"; /* rcv group event chat item */ -"invited via your group link" = "meghívta a csoport hivatkozásán keresztül"; +"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 kulcstár a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását."; +"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 kulcstár az alkalmazás újraindítása, vagy a jelmondat módosítása után a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását."; +"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." = "Ebben a csevegésben az üzenetek végleges törlése le van tiltva."; +"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 in this group." = "Ebben a csoportban az üzenetek végleges törlése le van tiltva."; +"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őprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; +"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 kapcsolata régi adatbázis biztonsági mentést használt."; +"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. Az üzenet visszafejtése sikertelen volt, mert vagy az ismerőse régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; +"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 (%@)."; @@ -2292,11 +2976,11 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japán kezelőfelület"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Csatlakozás"; /* No comment provided by engineer. */ -"join as %@" = "csatlakozás mint: %@"; +"join as %@" = "csatlakozás mint %@"; /* No comment provided by engineer. */ "Join group" = "Csatlakozás csoporthoz"; @@ -2314,28 +2998,31 @@ "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 az ön hivatkozása a(z) %@ csoporthoz!"; +"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" = "Megtart"; +"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"; -/* No comment provided by engineer. */ -"Keep unused invitation?" = "Fel nem használt meghívó megtartása?"; +/* 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" = "Kulcstároló hiba"; +"Keychain error" = "Kulcstartóhiba"; /* No comment provided by engineer. */ -"KeyChain error" = "Kulcstároló hiba"; +"KeyChain error" = "Kulcstartóhiba"; /* No comment provided by engineer. */ "Large file!" = "Nagy fájl!"; @@ -2343,20 +3030,26 @@ /* No comment provided by engineer. */ "Learn more" = "Tudjon meg többet"; +/* swipe action */ +"Leave" = "Elhagyás"; + /* No comment provided by engineer. */ -"Leave" = "Elhagy"; +"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?" = "Csoport elhagyása?"; +"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 Chat-ben"; +"Let's talk in SimpleX Chat" = "Beszélgessünk a SimpleX Chatben"; /* No comment provided by engineer. */ "Light" = "Világos"; @@ -2365,13 +3058,22 @@ "Limitations" = "Korlátozások"; /* No comment provided by engineer. */ -"Link mobile and desktop apps! 🔗" = "Társítsa össze a mobil és az asztali alkalmazásokat! 🔗"; +"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" = "Összekapcsolt számítógép beállítások"; +"Linked desktop options" = "Társított számítógép beállítások"; /* No comment provided by engineer. */ -"Linked desktops" = "Összekapcsolt számítógépek"; +"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Ő"; @@ -2382,9 +3084,6 @@ /* No comment provided by engineer. */ "Live messages" = "Élő üzenetek"; -/* No comment provided by engineer. */ -"Local" = "Helyi"; - /* No comment provided by engineer. */ "Local name" = "Helyi név"; @@ -2397,9 +3096,6 @@ /* No comment provided by engineer. */ "Lock mode" = "Zárolási mód"; -/* No comment provided by engineer. */ -"Make a private connection" = "Privát kapcsolat létrehozása"; - /* No comment provided by engineer. */ "Make one message disappear" = "Egy üzenet eltüntetése"; @@ -2407,32 +3103,32 @@ "Make profile private!" = "Tegye priváttá a profilját!"; /* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Győződjön meg arról, hogy a %@ szervercímek megfelelő formátumúak, sorszeparáltak és nem duplikáltak (%@)."; +"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. */ -"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 WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nem duplikáltak."; - -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Sokan kérdezték: *ha a SimpleX-nek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*"; - -/* No comment provided by engineer. */ -"Mark deleted for everyone" = "Jelölje meg mindenki számára töröltként"; +"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" = "Ellenőrzöttként jelölve"; +"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öröltnek jelölve"; +"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"; @@ -2440,46 +3136,118 @@ "Member" = "Tag"; /* profile update event chat item */ -"member %@ changed to %@" = "%1$@ megváltoztatta a nevét erre: %2$@"; +"member %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; /* rcv group event chat item */ "member connected" = "kapcsolódott"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: \"%@\". A csoport minden tagja értesítést kap róla."; +/* 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 \"%@\". The member will receive a new invitation." = "A tag szerepköre meg fog változni erre: \"%@\". A tag új meghívást fog kapni."; +"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 will be removed from group - this cannot be undone!" = "A tag eltávolítása a csoportból - ez a művelet nem vonható vissza!"; +"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 bizonylatok!"; +"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." = "Az üzenetreakciók ebben a csevegésben le vannak tiltva."; +"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 in this group." = "Ebben a csoportban az üzenetreakciók le vannak tiltva."; +"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 text" = "Üzenet szövege"; +"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"; @@ -2491,13 +3259,25 @@ "Messages & files" = "Üzenetek és fájlok"; /* No comment provided by engineer. */ -"Messages from %@ will be shown!" = "A(z) %@ által írt üzenetek megjelennek!"; +"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, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi."; +"Messages received" = "Fogadott üzenetek"; /* 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 üzeneteket, fájlokat és hívásokat **végpontok közötti kvantumrezisztens titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi."; +"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"; @@ -2518,22 +3298,22 @@ "Migrating" = "Átköltöztetés"; /* No comment provided by engineer. */ -"Migrating database archive…" = "Adatbázis archívum migrálása…"; +"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és hiba:"; +"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 az aktuális 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)."; +"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: %@"; +"Migrations:" = "Átköltöztetések:"; /* time unit */ "minutes" = "perc"; @@ -2548,80 +3328,107 @@ "moderated" = "moderált"; /* No comment provided by engineer. */ -"Moderated at" = "Moderálva lett ekkor:"; +"Moderated at" = "Moderálva"; /* copied message info */ -"Moderated at: %@" = "Moderálva lett ekkor: %@"; +"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örlésre került."; +"Most likely this connection is deleted." = "Valószínűleg ez a kapcsolat törölve lett."; /* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Valószínűleg ez az ismerős törölte önnel a kapcsolatot."; +"Multiple chat profiles" = "Több csevegési profil"; -/* No comment provided by engineer. */ -"Multiple chat profiles" = "Több csevegőprofil"; +/* notification label action */ +"Mute" = "Némítás"; -/* No comment provided by engineer. */ -"Mute" = "Elné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!"; -/* No comment provided by engineer. */ +/* 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" = "Internetkapcsolat"; +"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"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "soha"; +/* token status text */ +"New" = "Új"; + /* No comment provided by engineer. */ -"New chat" = "Új beszélgetés"; +"New chat" = "Új csevegés"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Új csevegési élmény 🎉"; /* notification */ -"New contact request" = "Új kapcsolattartási kérelem"; +"New contact request" = "Új meghívási kérés"; /* notification */ "New contact:" = "Új kapcsolat:"; /* No comment provided by engineer. */ -"New database archive" = "Új adatbázis-archívum"; +"New desktop app!" = "Új számítógép-alkalmazás!"; /* No comment provided by engineer. */ -"New desktop app!" = "Új asztali alkalmazás!"; +"New display name" = "Új megjelenítendő név"; -/* No comment provided by engineer. */ -"New display name" = "Új megjelenítési 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"; @@ -2637,6 +3444,15 @@ /* 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"; @@ -2647,16 +3463,28 @@ "No app password" = "Nincs alkalmazás jelszó"; /* No comment provided by engineer. */ -"No contacts selected" = "Nem kerültek ismerősök kiválasztásra"; +"No chats" = "Nincsenek csevegések"; /* No comment provided by engineer. */ -"No contacts to add" = "Nincs hozzáadandó ismerős"; +"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 eszköztoken!"; +"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"; @@ -2670,48 +3498,108 @@ /* 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. */ -"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (\"megfigyelő\" szerepkör)"; +"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" = "ki"; +group pref value +time to disappear */ +"off" = "kikapcsolva"; -/* No comment provided by engineer. */ -"Off" = "Ki"; +/* blur media */ +"Off" = "Kikapcsolva"; /* feature offered item */ "offered %@" = "%@ ajánlotta"; /* feature offered item */ -"offered %@: %@" = "ajánlotta %1$@: %2$@-kor"; +"offered %@: %@" = "ajánlotta: %1$@, ekkor: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Rendben"; /* No comment provided by engineer. */ @@ -2720,93 +3608,111 @@ /* No comment provided by engineer. */ "Old database" = "Régi adatbázis"; -/* No comment provided by engineer. */ -"Old database archive" = "Régi adatbázis archívum"; - /* group pref value */ -"on" = "be"; +"on" = "bekapcsolva"; /* No comment provided by engineer. */ -"One-time invitation link" = "Egyszer használatos meghívó hivatkozás"; +"One-time invitation link" = "Egyszer használható meghívó"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "A kapcsolódáshoz Onion kiszolgálókra lesz szükség. VPN engedélyezése szükséges."; +"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. Requires enabling VPN." = "Onion kiszolgálók használata, ha azok rendelkezésre állnak. VPN engedélyezése szükséges."; +"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." = "Onion kiszolgálók nem lesznek használva."; +"Onion hosts will not be used." = "Az onion-kiszolgálók nem lesznek használva."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Csak a klienseszközök tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végponttól-végpontig titkosítással** küldött üzeneteket."; +"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 group owners can change group preferences." = "Csak a csoporttulajdonosok módosíthatják a csoportbeállításokat."; +"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 group owners can enable files and media." = "Csak a csoporttulajdonosok engedélyezhetik a fájlok- és a médiatartalmak küldését."; +"Only delete conversation" = "Csak a beszélgetés törlése"; /* No comment provided by engineer. */ -"Only group owners can enable voice messages." = "Csak a csoporttulajdonosok engedélyezhetik a hangüzenetek küldését."; +"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 you can add message reactions." = "Csak ön adhat hozzá üzenetreakciókat."; +"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 you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Véglegesen csak ön törölhet üzeneteket (ismerőse csak törlésre jelölheti meg őket ). (24 óra)"; +"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 you can make calls." = "Csak ön tud hívásokat indítani."; +"Only sender and moderators see it" = "Csak a küldő és a moderátorok látják"; /* No comment provided by engineer. */ -"Only you can send disappearing messages." = "Csak ön tud eltűnő üzeneteket küldeni."; +"Only you and moderators see it" = "Csak Ön és a moderátorok látják"; /* No comment provided by engineer. */ -"Only you can send voice messages." = "Csak ön tud hangüzeneteket küldeni."; +"Only you can add message reactions." = "Csak Ön adhat hozzá reakciókat az üzenetekhez."; /* No comment provided by engineer. */ -"Only your contact can add message reactions." = "Csak az ismerős tud üzeneteakciókat adni."; +"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 your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Csak az ismerőse tudja az üzeneteket véglegesen törölni (ön csak törlésre jelölheti meg azokat). (24 óra)"; +"Only you can make calls." = "Csak Ön tud hívásokat indítani."; /* No comment provided by engineer. */ -"Only your contact can make calls." = "Csak az ismerős tud hívást indítani."; +"Only you can send disappearing messages." = "Csak Ön tud eltűnő üzeneteket küldeni."; /* No comment provided by engineer. */ -"Only your contact can send disappearing messages." = "Csak az ismerős tud eltűnő üzeneteket küldeni."; +"Only you can send voice messages." = "Csak Ön tud hangüzeneteket küldeni."; /* No comment provided by engineer. */ -"Only your contact can send voice messages." = "Csak az ismerős tud hangüzeneteket küldeni."; +"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és konzol megnyitása"; +"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 megkezdése egy másik eszközre"; +"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"; -/* authentication reason */ -"Open user profiles" = "Felhasználói profilok megnyitása"; - -/* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Nyílt forráskódú protokoll és forráskód – bárki üzemeltethet kiszolgálókat."; - /* 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"; @@ -2814,14 +3720,29 @@ "Or scan QR code" = "Vagy QR-kód beolvasása"; /* No comment provided by engineer. */ -"Or securely share this file link" = "Vagy a fájl hivítkozásának biztonságos megosztása"; +"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"; @@ -2832,25 +3753,28 @@ "Passcode" = "Jelkód"; /* No comment provided by engineer. */ -"Passcode changed!" = "A jelkód megváltozott!"; +"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 változott!"; +"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 to show" = "Jelszó megjelenítése"; - -/* past/unknown group member */ -"Past member %@" = "Korábbi csoport tag %@"; +"Password" = "Jelszó"; /* No comment provided by engineer. */ -"Paste desktop address" = "Számítógép azonosítójának beillesztése"; +"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"; @@ -2859,16 +3783,22 @@ "Paste link to connect!" = "Hivatkozás beillesztése a kapcsolódáshoz!"; /* No comment provided by engineer. */ -"Paste the link you received" = "Fogadott hivatkozás beillesztése"; +"Paste the link you received" = "Kapott hivatkozás beillesztése"; /* No comment provided by engineer. */ -"peer-to-peer" = "ponttól-pontig"; +"peer-to-peer" = "egyenrangú"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Az emberek csak az ön által megosztott hivatkozáson keresztül kapcsolódhatnak."; +"pending" = "függőben"; /* No comment provided by engineer. */ -"Periodically" = "Rendszeresen"; +"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"; @@ -2877,56 +3807,77 @@ "Picture-in-picture calls" = "Kép a képben hívások"; /* No comment provided by engineer. */ -"PING count" = "PING számláló"; +"PING count" = "PING-ek száma"; /* No comment provided by engineer. */ -"PING interval" = "PING időköze"; +"PING interval" = "Időtartam a PING-ek között"; /* No comment provided by engineer. */ -"Please ask your contact to enable sending voice messages." = "Ismerős felkérése, hogy engedélyezze a hangüzenetek küldését."; +"Play from the chat list." = "Lejátszás a csevegési listából."; /* 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 ismerősét, hogy küldjön egy másikat."; +"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 check your network connection with %@ and try again." = "Ellenőrizze hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra."; +"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 yours and your contact preferences." = "Ellenőrizze az ön és ismerőse beállításait."; +"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 confirm that network settings are correct for this device." = "Ellenőrizze, hogy a hálózati beállítások megfelelőek-e ehhez az eszközhöz."; +"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 adminnal."; +"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 aktuális jelmondatát."; +"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." = "Előző jelszó megadása az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza."; +"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!"; +"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."; +"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 megváltoztatni."; +"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"; @@ -2934,26 +3885,53 @@ "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" = "Előre beállított kiszolgáló"; +"Preset server address" = "Az előre beállított kiszolgáló címe"; /* No comment provided by engineer. */ -"Preset server address" = "Előre beállított kiszolgáló címe"; +"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 redefined" = "Adatvédelem újraértelmezve"; +"Privacy for your customers." = "Saját ügyfeleinek adatvédelme."; /* No comment provided by engineer. */ -"Private filenames" = "Privát fájl nevek"; +"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"; @@ -2963,86 +3941,104 @@ /* No comment provided by engineer. */ "Profile images" = "Profilképek"; -/* No comment provided by engineer. */ -"Profile name" = "Profilnév"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profil neve:"; - /* No comment provided by engineer. */ "Profile password" = "Profiljelszó"; /* No comment provided by engineer. */ -"Profile update will be sent to your contacts." = "A profilfrissítés elküldésre került az ismerősök számára."; +"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." = "Hang- és videóhívások tiltása."; +"Prohibit audio/video calls." = "A hívások kezdeményezése le van tiltva."; /* No comment provided by engineer. */ -"Prohibit irreversible message deletion." = "Az üzenetek véglegesen való törlése le van tiltva."; +"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." = "Üzenetreakciók tiltása."; +"Prohibit message reactions." = "A reakciók hozzáadása az üzenethez le van tiltva."; /* No comment provided by engineer. */ -"Prohibit messages reactions." = "Az üzenetreakciók tiltása."; +"Prohibit messages reactions." = "A reakciók hozzáadása az üzenetekhez le van tiltva."; /* No comment provided by engineer. */ -"Prohibit sending direct messages to members." = "Közvetlen üzenetek küldésének letiltása a tagok számára."; +"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 disappearing messages." = "Eltűnő üzenetek küldésének letiltása."; +"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 files and media." = "Fájlok- és a médiatartalom küldés letiltása."; +"Prohibit sending disappearing messages." = "Az eltűnő üzenetek 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."; +"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 voice messages." = "Hangüzenetek küldésének letiltása."; +"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 your chat profiles with a password!" = "Csevegési profiljok védelme jelszóval!"; +"Protect IP address" = "IP-cím védelme"; /* No comment provided by engineer. */ -"Protocol timeout" = "Protokoll időtúllépés"; +"Protect your chat profiles with a password!" = "Védje meg a csevegési profiljait egy jelszóval!"; /* No comment provided by engineer. */ -"Protocol timeout per KB" = "Protokoll időkorlát KB-onként"; +"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. */ -"Push notifications" = "Push értesítések"; +"Protocol timeout" = "Protokoll időtúllépése"; /* No comment provided by engineer. */ -"Push server" = "Push kiszolgáló"; +"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 kvantumrezisztens titkosítás"; +"quantum resistant e2e encryption" = "végpontok közötti kvantumbiztos titkosítás"; /* No comment provided by engineer. */ -"Quantum resistant encryption" = "Kvantumrezisztens titkosítás"; +"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…"; -/* No comment provided by engineer. */ -"Read" = "Olvasd el"; +/* 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/app-settings.html#your-simplex-contact-address)." = "További információ a [Használati útmutatóban](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)." = "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/chat-profiles.html#incognito-mode)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](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)."; @@ -3051,40 +4047,46 @@ "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. */ -"Read more in our GitHub repository." = "További információ a GitHub tárolónkban."; +"Receipts are disabled" = "A kézbesítési jelentések le vannak tiltva"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Üzenet kézbesítési jelentés letiltva"; +"Receive errors" = "Üzenetfogadási hibák"; /* No comment provided by engineer. */ -"received answer…" = "fogadott válasz…"; +"received answer…" = "válasz fogadása…"; /* No comment provided by engineer. */ -"Received at" = "Fogadva ekkor:"; +"Received at" = "Fogadva"; /* copied message info */ -"Received at: %@" = "Fogadva ekkor: %@"; +"Received at: %@" = "Fogadva: %@"; /* No comment provided by engineer. */ "received confirmation…" = "visszaigazolás fogadása…"; /* notification */ -"Received file event" = "Fogadott fájl esemény"; +"Received file event" = "Fogadott fájlesemény"; /* message info title */ -"Received message" = "Fogadott üzenet"; +"Received message" = "Fogadott üzenetbuborék színe"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be."; +"Received messages" = "Fogadott üzenetek"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Egyidejű fogadás"; +"Received reply" = "Fogadott válaszüzenet-buborék színe"; /* No comment provided by engineer. */ -"Receiving file will be stopped." = "A fájl fogadása leállt."; +"Received total" = "Összes fogadott üzenet"; /* No comment provided by engineer. */ -"Receiving via" = "Fogadás a"; +"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)."; @@ -3093,79 +4095,113 @@ "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 frissítéseket."; +"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 all connected servers to force message delivery. It uses additional traffic." = "Újrakapcsolódás az összes kiszolgálóhoz az üzenetek kézbesítésének kikényszerítéséhez. Ez további forgalmat használ."; +"Reconnect" = "Újrakapcsolódás"; /* No comment provided by engineer. */ -"Reconnect servers?" = "Újrakapcsolódás a kiszolgálókhoz?"; +"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. */ -"Record updated at" = "A bejegyzés frissítve"; +"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: %@" = "A bejegyzés frissítve: %@"; +"Record updated at: %@" = "Bejegyzés frissítve: %@"; /* No comment provided by engineer. */ -"Reduced battery usage" = "Csökkentett akkumulátorhasználat"; +"Reduced battery usage" = "Csökkentett akkumulátor-használat"; -/* reject incoming call via notification */ +/* 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" = "Kapcsolatfelvételi kérelem elutasítása"; +"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." = "Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címét."; +"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." = "Az átjátszó kiszolgáló megvédi IP-címét, de megfigyelheti a hívás időtartamát."; +"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 member" = "Tag eltávolítása"; +"Remove archive?" = "Eltávolítja az archívumot?"; /* No comment provided by engineer. */ -"Remove member?" = "Tag eltávolítása?"; +"Remove image" = "Kép eltávolítása"; /* No comment provided by engineer. */ -"Remove passphrase from keychain?" = "Jelmondat eltávolítása a kulcstárolóból?"; +"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ítva"; +"removed %@" = "eltávolította őt: %@"; /* profile update event chat item */ -"removed contact address" = "törölt kapcsolattartási azonosító"; +"removed contact address" = "eltávolította a kapcsolattartási címet"; /* profile update event chat item */ -"removed profile picture" = "törölt profilkép"; +"removed profile picture" = "eltávolította a profilképét"; /* rcv group event chat item */ -"removed you" = "eltávolítottak"; +"removed you" = "eltávolította Önt"; /* No comment provided by engineer. */ -"Renegotiate" = "Újraegyzetetés"; +"Renegotiate" = "Újraegyeztetés"; /* No comment provided by engineer. */ "Renegotiate encryption" = "Titkosítás újraegyeztetése"; /* No comment provided by engineer. */ -"Renegotiate encryption?" = "Titkosítás újraegyeztetése?"; +"Renegotiate encryption?" = "Újraegyezteti a titkosítást?"; /* No comment provided by engineer. */ -"Repeat connection request?" = "Kapcsolódási kérés megismétlése?"; +"Repeat connection request?" = "Megismétli a meghívási kérést?"; /* No comment provided by engineer. */ "Repeat download" = "Letöltés ismét"; @@ -3174,7 +4210,7 @@ "Repeat import" = "Importálás ismét"; /* No comment provided by engineer. */ -"Repeat join request?" = "Csatlakozási kérés megismétlése?"; +"Repeat join request?" = "Megismétli a meghívási kérést?"; /* No comment provided by engineer. */ "Repeat upload" = "Feltöltés ismét"; @@ -3182,17 +4218,65 @@ /* chat item action */ "Reply" = "Válasz"; -/* No comment provided by engineer. */ -"Required" = "Megkövetelt"; +/* 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. */ -"Reset" = "Alaphelyzetbe állítás"; +"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. */ -"Reset colors" = "Színek alaphelyzetbe állítása"; +"Reporting messages to moderators is prohibited." = "Az üzenetek jelentése a moderátorok felé le van tiltva."; /* No comment provided by engineer. */ -"Reset to defaults" = "Alaphelyzetbe állítás"; +"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"; @@ -3207,10 +4291,10 @@ "Restore database backup" = "Adatbázismentés visszaállítása"; /* No comment provided by engineer. */ -"Restore database backup?" = "Adatbázismentés visszaállítása?"; +"Restore database backup?" = "Visszaállítja az adatbázismentést?"; /* No comment provided by engineer. */ -"Restore database error" = "Hiba az adatbázis visszaállításakor"; +"Restore database error" = "Hiba történt az adatbázis visszaállításakor"; /* No comment provided by engineer. */ "Retry" = "Újrapróbálkozás"; @@ -3219,7 +4303,7 @@ "Reveal" = "Felfedés"; /* No comment provided by engineer. */ -"Revert" = "Visszaállít"; +"Review conditions" = "Feltételek felülvizsgálata"; /* No comment provided by engineer. */ "Revoke" = "Visszavonás"; @@ -3228,7 +4312,7 @@ "Revoke file" = "Fájl visszavonása"; /* No comment provided by engineer. */ -"Revoke file?" = "Fájl visszavonása?"; +"Revoke file?" = "Visszavonja a fájlt?"; /* No comment provided by engineer. */ "Role" = "Szerepkör"; @@ -3236,56 +4320,60 @@ /* 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"; -/* chat item action */ +/* alert button +chat item action */ "Save" = "Mentés"; -/* No comment provided by engineer. */ -"Save (and notify contacts)" = "Mentés (és az ismerősök értesítése)"; +/* alert button */ +"Save (and notify contacts)" = "Mentés (és a partnerek értesítése)"; -/* No comment provided by engineer. */ -"Save and notify contact" = "Mentés és ismerős é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 update group profile" = "Mentés és a csoport profil frissítése"; +"Save and reconnect" = "Mentés és újrakapcsolódás"; /* No comment provided by engineer. */ -"Save archive" = "Archívum mentése"; +"Save and update group profile" = "Mentés és a csoportprofil frissítése"; /* No comment provided by engineer. */ -"Save auto-accept settings" = "Automatikus elfogadási beállítások mentése"; +"Save group profile" = "Csoportprofil mentése"; /* No comment provided by engineer. */ -"Save group profile" = "Csoport profil elmentése"; +"Save list" = "Lista mentése"; /* No comment provided by engineer. */ -"Save passphrase and open chat" = "Jelmondat elmentése és csevegés megnyitása"; +"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 kulcstárban"; +"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 preferences?" = "Beállítások mentése?"; - -/* No comment provided by engineer. */ -"Save profile password" = "Felhasználói fiók jelszavának mentése"; +"Save profile password" = "Profiljelszó mentése"; /* No comment provided by engineer. */ "Save servers" = "Kiszolgálók 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 settings?" = "Beállítások mentése?"; +"Save welcome message?" = "Menti az üdvözlőüzenetet?"; -/* No comment provided by engineer. */ -"Save welcome message?" = "Üdvözlőszöveg mentése?"; +/* alert title */ +"Save your profile?" = "Menti a profilt?"; /* No comment provided by engineer. */ "saved" = "mentett"; @@ -3294,44 +4382,59 @@ "Saved" = "Mentett"; /* No comment provided by engineer. */ -"Saved from" = "Mentve innen:"; +"Saved from" = "Elmentve innen"; /* No comment provided by engineer. */ -"saved from %@" = "mentve innen: %@"; +"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 eltávolításra kerülnek"; +"Saved WebRTC ICE servers will be removed" = "A mentett WebRTC ICE-kiszolgálók el lesznek távolítva"; /* No comment provided by engineer. */ -"Scan code" = "Kód beolvasása"; +"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 számítógépről"; +"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 ismerős általi alkalmazásból."; +"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ó hivatkozásokat."; +"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"; +"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"; @@ -3339,7 +4442,10 @@ "secret" = "titok"; /* server test step */ -"Secure queue" = "Biztonságos várólista"; +"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"; @@ -3348,28 +4454,37 @@ "Security code" = "Biztonsági kód"; /* chat item text */ -"security code changed" = "a biztonsági kód megváltozott"; +"security code changed" = "a biztonsági kód módosult"; + +/* chat item action */ +"Select" = "Kijelölés"; /* No comment provided by engineer. */ -"Select" = "Választás"; +"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ési jelkód"; +"Self-destruct passcode" = "Önmegsemmisítő-jelkód"; /* No comment provided by engineer. */ -"Self-destruct passcode changed!" = "Az önmegsemmisítési jelkód megváltozott!"; +"Self-destruct passcode changed!" = "Az önmegsemmisítő-jelkód módosult!"; /* No comment provided by engineer. */ -"Self-destruct passcode enabled!" = "Az önmegsemmisítési jelkód engedélyezve!"; +"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 - a címzett(ek) számára frissül, ahogy beírja"; +"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"; @@ -3377,9 +4492,6 @@ /* No comment provided by engineer. */ "send direct message" = "közvetlen üzenet küldése"; -/* 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"; @@ -3387,86 +4499,155 @@ "Send disappearing message" = "Eltűnő üzenet küldése"; /* No comment provided by engineer. */ -"Send link previews" = "Hivatkozás előnézetek küldése"; +"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 notifications:" = "Értesítések küldése:"; +"Send private reports" = "Privát jelentések küldése"; /* No comment provided by engineer. */ -"Send questions and ideas" = "Ötletek és kérdések beküldése"; +"Send questions and ideas" = "Ötletek és javaslatok"; /* No comment provided by engineer. */ -"Send receipts" = "Üzenet kézbesítési jelentések"; +"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 galériából vagy egyedi billentyűzetekről."; +"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." = "Az utolsó 100 üzenet elküldése az új tagoknak."; +"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 cancelled file transfer." = "A küldő megszakította a fájl átvitelt."; +"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. */ -"Sender may have deleted the connection request." = "A küldő törölhette a kapcsolódási kérelmet."; +"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 in all visible chat profiles." = "A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő minden ismerős számára."; +"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 delivery receipts will be enabled for all contacts." = "A kézbesítési jelentés küldése minden ismerős számára engedélyezésre kerül."; +"Sending file will be stopped." = "A fájl küldése le fog állni."; /* No comment provided by engineer. */ -"Sending file will be stopped." = "A fájl küldése leállt."; +"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 contacts" = "A kézbesítési jelentések küldése le van tiltva %lld ismerősnél"; +"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 disabled for %lld groups" = "A kézbesítési jelentések küldése le van tiltva %lld csoportban"; +"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 contacts" = "A kézbesítési jelentések küldése engedélyezve van %lld ismerős számára"; +"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 receipts is enabled for %lld groups" = "A kézbesítési jelentések küldése engedélyezve van %lld csoportban"; +"Sending via" = "Küldés a következőn keresztül:"; /* No comment provided by engineer. */ -"Sending via" = "Küldés ezen keresztül"; - -/* No comment provided by engineer. */ -"Sent at" = "Elküldve ekkor:"; +"Sent at" = "Elküldve"; /* copied message info */ -"Sent at: %@" = "Elküldve ekkor: %@"; +"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ájl esemény"; +"Sent file event" = "Elküldött fájlesemény"; /* message info title */ -"Sent message" = "Elküldött üzenet"; +"Sent message" = "Üzenetbuborék színe"; /* No comment provided by engineer. */ -"Sent messages will be deleted after set time." = "Az elküldött üzenetek törlésre kerülnek a beállított idő után."; - -/* server test error */ -"Server requires authorization to create queues, check password" = "A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát"; - -/* server test error */ -"Server requires authorization to upload, check password" = "A kiszolgálónak engedélyre van szüksége a várólisták feltöltéséhez, ellenőrizze jelszavát"; +"Sent messages" = "Elküldött üzenetek"; /* No comment provided by engineer. */ -"Server test failed!" = "Sikertelen kiszolgáló-teszt!"; +"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"; @@ -3474,19 +4655,28 @@ "Set 1 day" = "Beállítva 1 nap"; /* No comment provided by engineer. */ -"Set contact name…" = "Ismerős nevének beállítása…"; +"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." = "Rendszerhitelesítés helyetti beállítás."; +"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 azonosító beállítása"; +"set new contact address" = "új kapcsolattartási címet állított be"; /* profile update event chat item */ -"set new profile picture" = "új profilkép beállítása"; +"set new profile picture" = "új profilképet állított be"; /* No comment provided by engineer. */ "Set passcode" = "Jelkód beállítása"; @@ -3498,7 +4688,7 @@ "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ő üzenet beállítása az új tagok számára!"; +"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"; @@ -3506,29 +4696,57 @@ /* No comment provided by engineer. */ "Settings" = "Beállítások"; -/* No comment provided by engineer. */ -"Shape profile images" = "Profilkép alakzat"; +/* alert message */ +"Settings were changed." = "A beállítások módosultak."; -/* chat item action */ +/* 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álatos hivatkozás megosztása"; +"Share 1-time link" = "Egyszer használható meghívó megosztása"; /* No comment provided by engineer. */ -"Share address" = "Azonosító megosztása"; +"Share 1-time link with a friend" = "Egyszer használható meghívó megosztása egy baráttal"; /* No comment provided by engineer. */ -"Share address with contacts?" = "Megosztja az azonosítót az ismerőseivel?"; +"Share address" = "Cím megosztása"; /* No comment provided by engineer. */ -"Share link" = "Hivatkozás megosztása"; +"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 this 1-time invite link" = "Egyszer használatos meghívó hivatkozás megosztása"; +"Share from other apps." = "Megosztás más alkalmazásokból."; /* No comment provided by engineer. */ -"Share with contacts" = "Megosztás ismerősökkel"; +"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"; @@ -3537,61 +4755,88 @@ "Show developer options" = "Fejlesztői beállítások megjelenítése"; /* No comment provided by engineer. */ -"Show last messages" = "Utolsó üzenetek megjelenítése"; +"Show last messages" = "Legutóbbi üzenet előnézetének megjelenítése"; /* No comment provided by engineer. */ -"Show preview" = "Előnézet megjelenítése"; +"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ítés:"; +"Show:" = "Megjelenítve:"; /* No comment provided by engineer. */ -"SimpleX address" = "SimpleX azonosító"; +"SimpleX" = "SimpleX"; /* No comment provided by engineer. */ -"SimpleX Address" = "SimpleX azonosító"; +"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 azonosító"; +"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 csoport hivatkozás"; +"SimpleX group link" = "SimpleX-csoporthivatkozás"; /* chat feature */ -"SimpleX links" = "SimpleX hivatkozások"; +"SimpleX links" = "SimpleX-hivatkozások"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "A SimpleX hivatkozások küldése ebben a csoportban le van tiltva."; +"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"; +"SimpleX links not allowed" = "A SimpleX-hivatkozások küldése le van tiltva"; /* No comment provided by engineer. */ -"SimpleX Lock" = "SimpleX zárolás"; +"SimpleX Lock" = "SimpleX-zár"; /* No comment provided by engineer. */ -"SimpleX Lock mode" = "SimpleX zárolási mód"; +"SimpleX Lock mode" = "Zárolási mód"; /* No comment provided by engineer. */ -"SimpleX Lock not enabled!" = "SimpleX zárolás nincs engedélyezve!"; +"SimpleX Lock not enabled!" = "A SimpleX-zár nincs bekapcsolva!"; /* No comment provided by engineer. */ -"SimpleX Lock turned on" = "SimpleX zárolás bekapcsolva"; +"SimpleX Lock turned on" = "SimpleX-zár bekapcsolva"; /* simplex link type */ -"SimpleX one-time invitation" = "SimpleX egyszer használatos meghívó"; +"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó"; /* No comment provided by engineer. */ -"Simplified incognito mode" = "Egyszerűsített inkognító mód"; +"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"; @@ -3603,14 +4848,36 @@ "Small groups (max 20)" = "Kis csoportok (max. 20 tag)"; /* No comment provided by engineer. */ -"SMP servers" = "Üzenetküldő (SMP) kiszolgálók"; +"SMP server" = "SMP-kiszolgáló"; /* 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ás során – további részletekért a csevegési konzolban olvashat."; +"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."; @@ -3621,14 +4888,20 @@ "Start chat" = "Csevegés indítása"; /* No comment provided by engineer. */ -"Start chat?" = "Csevegés indítása?"; +"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"; @@ -3636,28 +4909,25 @@ "Stop chat" = "Csevegési szolgáltatás megállítása"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Csevegés megállítása az adatbázis-műveletek engedélyezéséhez"; +"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 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ő adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni."; - -/* No comment provided by engineer. */ -"Stop chat?" = "Csevegési szolgáltatás megállítása?"; +"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?" = "Fájl fogadás megállítása?"; +"Stop receiving file?" = "Megállítja a fájlfogadást?"; /* No comment provided by engineer. */ -"Stop sending file?" = "Fájl küldés megállítása?"; +"Stop sending file?" = "Megállítja a fájlküldést?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Megosztás megállítása"; -/* No comment provided by engineer. */ -"Stop sharing address?" = "Címmegosztá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"; @@ -3665,50 +4935,80 @@ /* 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. */ -"Support SimpleX Chat" = "Támogassa a SimpleX Chatet"; +"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" = "Rendszerhitelesítés"; +"System authentication" = "Rendszer-hitelesítés"; /* No comment provided by engineer. */ -"Take picture" = "Fotó készítése"; +"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 a kapcsolódáshoz"; +"Tap to Connect" = "Koppintson ide a kapcsolódáshoz"; /* No comment provided by engineer. */ -"Tap to join" = "Koppintson a csatlakozáshoz"; +"Tap to join" = "Koppintson ide a csatlakozáshoz"; /* No comment provided by engineer. */ -"Tap to join incognito" = "Koppintson az inkognitóban való csatlakozáshoz"; +"Tap to join incognito" = "Koppintson ide az inkognitóban való kapcsolódáshoz"; /* No comment provided by engineer. */ -"Tap to paste link" = "Koppintson a hivatkozás beillesztéséhez"; +"Tap to paste link" = "Koppintson ide a hivatkozás beillesztéséhez"; /* No comment provided by engineer. */ -"Tap to scan" = "Koppintson a beolvasáshoz"; +"Tap to scan" = "Koppintson ide a QR-kód beolvasásához"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Koppintson az új csevegés indításához"; +"TCP connection" = "TCP-kapcsolat"; /* No comment provided by engineer. */ -"TCP connection timeout" = "TCP kapcsolat időtúllépés"; +"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"; @@ -3719,8 +5019,14 @@ /* 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 sikertelen volt a(z) %@ lépésnél."; +"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"; @@ -3728,62 +5034,80 @@ /* No comment provided by engineer. */ "Test servers" = "Kiszolgálók tesztelése"; -/* No comment provided by engineer. */ +/* 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 – [hozzájárulás a Weblate-en keresztül](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +"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 - hozzájárulás a Weblaten!"; +"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 1st platform without any user identifiers – private by design." = "Az első csevegési rendszer bármiféle felhasználó azonosító nélkül - privátra lett tervezre."; +"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 can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatfelvételi kéréseket kap – beállítások megnyitása az engedélyezéshez."; +"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 attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be."; +"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 code you scanned is not a SimpleX link QR code." = "A beolvasott kód nem egy SimpleX hivatkozás QR-kód."; +"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 connection you accepted will be cancelled!" = "Az ön által elfogadott kapcsolat megszakad!"; +"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 contact you shared this link with will NOT be able to connect!" = "Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni!"; +"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 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üpontban érhető el."; +"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 hash of the previous message is different." = "Az előző üzenet hash-e más."; +"The future of messaging" = "Az üzenetváltás jövője"; /* 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 hibás (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő."; +"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 message will be deleted for all members." = "Az üzenet minden tag számára törlésre kerül."; +"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 marked as moderated for all members." = "Az üzenet minden tag számára moderáltként lesz megjelölve."; +"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 next generation of private messaging" = "A privát üzenetküldés következő generációja"; +"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 old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem került eltávolításra az átköltöztetés során, így törölhető."; +"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 profile is only shared with your contacts." = "Profilja csak az ismerőseivel kerül megosztásra."; +"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! ✅"; @@ -3792,64 +5116,88 @@ "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 **%@**." = "Jelenlegi profil új ismerőseinek kiszolgálói **%@**."; +"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 text you pasted is not a SimpleX link." = "A beillesztett szöveg nem egy SimpleX hivatkozás."; +"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. */ -"Theme" = "Téma"; +"The text you pasted is not a SimpleX link." = "A beillesztett szöveg nem egy SimpleX-hivatkozás."; /* No comment provided by engineer. */ -"These settings are for your current profile **%@**." = "Ezek a beállítások a jelenlegi **%@** profiljára vonatkoznak."; +"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. */ -"They can be overridden in contact and group settings." = "Ezek felülbírálhatóak az ismerős- és csoportbeállításokban."; +"Themes" = "Témák"; /* 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édiatartalommal együtt törlésre kerülnek. Az alacsony felbontású fotók viszont megmaradnak."; +"These conditions will also apply for: **%@**." = "Ezek a feltételek lesznek elfogadva a következő számára is: **%@**."; /* 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 kiválasztottnál korábban küldött és fogadott üzenetek törlésre kerülnek. Ez több percet is igénybe vehet."; +"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. */ -"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Ez a művelet nem vonható vissza - profiljai, ismerősei, üzenetei és fájljai visszafordíthatatlanul törlésre kerülnek."; +"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 kvantumrezisztens tikosítással védett."; +"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 az ismerős"; +"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ített felhasználónév érvénytelen. Válasszon egy másik nevet."; +"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 kerülnek elküldésre."; +"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 az egyszer használatos hivatkozása!"; +"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 SimpleX azonosítója!"; +"This is your own SimpleX address!" = "Ez a saját SimpleX-címe!"; /* No comment provided by engineer. */ -"This setting applies to messages in your current chat profile **%@**." = "Ez a beállítás a jelenlegi **%@** profiljában lévő üzenetekre érvényes."; +"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 az ismerőse beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást."; +"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."; @@ -3858,37 +5206,73 @@ "To make a new connection" = "Új kapcsolat létrehozásához"; /* 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." = "Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználói azonosítók helyett, a SimpleX üzenetsorokhoz rendel azonosítókat, minden egyes ismerőshöz egy különbözőt."; +"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édelme érdekében a kép-/hangfájlok UTC-t használnak."; +"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." = "Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót.\nA funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére."; +"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 feltárásához írja be a teljes jelszót a keresőmezőbe a **Csevegési profiljai** oldalon."; +"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 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 migrálása szükséges."; +"To send" = "A küldéshez"; /* No comment provided by engineer. */ -"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás ellenőrzéséhez ismerősével hasonlítsa össze (vagy szkennelje be) az eszközén lévő kódot."; +"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. */ -"Toggle incognito when connecting." = "Inkognitó mód kapcsolódáskor."; +"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. */ -"Transport isolation" = "Kapcsolat izolációs mód"; +"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. */ -"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 ismerőstől érkező üzenetek fogadására szolgál (hiba: %@)."; +"Toggle chat list:" = "Csevegési lista átváltása:"; /* 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 ismerőstől érkező üzenetek fogadására szolgál."; +"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"; @@ -3906,28 +5290,28 @@ "Unblock" = "Feloldás"; /* No comment provided by engineer. */ -"Unblock for all" = "Letiltás feloldása mindenki számára"; +"Unblock for all" = "Feloldás"; /* No comment provided by engineer. */ "Unblock member" = "Tag feloldása"; /* No comment provided by engineer. */ -"Unblock member for all?" = "Mindenki számára feloldja a tag letiltását?"; +"Unblock member for all?" = "Az összes tag számára feloldja a tag letiltását?"; /* No comment provided by engineer. */ -"Unblock member?" = "Tag feloldása?"; +"Unblock member?" = "Feloldja a tag letiltását?"; /* rcv group event chat item */ -"unblocked %@" = "%@ feloldva"; +"unblocked %@" = "feloldotta %@ letiltását"; -/* item status description */ -"Unexpected error: %@" = "Váratlan hiba: %@"; +/* 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"; -/* No comment provided by engineer. */ -"Unfav." = "Nem kedvelt."; +/* swipe action */ +"Unfav." = "Kedvenc megszüntetése"; /* No comment provided by engineer. */ "Unhide" = "Felfedés"; @@ -3954,19 +5338,25 @@ "Unknown error" = "Ismeretlen hiba"; /* No comment provided by engineer. */ -"unknown status" = "ismeretlen státusz"; +"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 az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e."; +"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?" = "Számítógép szétkapcsolása?"; +"Unlink desktop?" = "Leválasztja a számítógépet?"; /* No comment provided by engineer. */ "Unlock" = "Feloldás"; @@ -3974,44 +5364,50 @@ /* authentication reason */ "Unlock app" = "Alkalmazás feloldása"; -/* No comment provided by engineer. */ -"Unmute" = "Némítá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. */ -"Up to 100 last messages are sent to new members." = "Legfeljebb az utolsó 100 üzenet kerül elküldésre az új tagok számára."; +"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 .onion hosts setting?" = "Tor .onion kiszolgálók beállításainak frissítése?"; +"Update database passphrase" = "Az adatbázis jelmondatának módosítása"; /* No comment provided by engineer. */ -"Update database passphrase" = "Adatbázis jelmondat megváltoztatása"; +"Update network settings?" = "Módosítja a hálózati beállításokat?"; /* No comment provided by engineer. */ -"Update network settings?" = "Hálózati beállítások megváltoztatása?"; +"Update settings?" = "Frissíti a beállításokat?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Kapcsolat izolációs mód frissítése?"; +"Updated conditions" = "Frissített feltételek"; /* rcv group event chat item */ -"updated group profile" = "módosított csoport profil"; +"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 szerverekhez újra kapcsolódással jár."; +"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. */ -"Updating this setting will re-connect the client to all servers." = "A beállítás frissítésével a kliens újrakapcsolódik az összes kiszolgálóhoz."; +"Upgrade and open chat" = "Fejlesztés és a csevegés megnyitása"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "A csevegés frissítése és megnyitása"; +"Upload errors" = "Feltöltési hibák"; /* No comment provided by engineer. */ "Upload failed" = "Sikertelen feltöltés"; @@ -4019,50 +5415,92 @@ /* 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" = "Tor .onion kiszolgálók használata"; +"Use .onion hosts" = "Onion-kiszolgálók használata"; /* No comment provided by engineer. */ -"Use chat" = "Csevegés használata"; +"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" = "Használat számítógépről"; +"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ófelület használata"; +"Use iOS call interface" = "Az iOS hívási felületét használata"; /* No comment provided by engineer. */ -"Use new incognito profile" = "Az új inkognító profil használata"; +"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 SimpleX Chat servers?" = "SimpleX Chat kiszolgálók használata?"; +"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. */ -"User profile" = "Felhasználói profil"; +"Use the app with one hand." = "Használja az alkalmazást egy kézzel."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "A .onion kiszolgálók használatához kompatibilis VPN szolgáltatóra van szükség."; +"Use web port" = "Webport használata"; /* No comment provided by engineer. */ -"Using SimpleX Chat servers." = "SimpleX Chat kiszolgálók használatban."; +"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%@"; @@ -4071,43 +5509,46 @@ "v%@ (%@)" = "v%@ (%@)"; /* No comment provided by engineer. */ -"Verify code with desktop" = "Kód ellenőrzése a számítógépen"; +"Verify code with desktop" = "Kód hitelesítése a számítógépen"; /* No comment provided by engineer. */ -"Verify connection" = "Kapcsolat ellenőrzése"; +"Verify connection" = "Kapcsolat hitelesítése"; /* No comment provided by engineer. */ -"Verify connection security" = "Kapcsolat biztonságának ellenőrzése"; +"Verify connection security" = "Biztonságos kapcsolat hitelesítése"; /* No comment provided by engineer. */ -"Verify connections" = "Kapcsolatok ellenőrzése"; +"Verify connections" = "Kapcsolatok hitelesítése"; /* No comment provided by engineer. */ -"Verify database passphrase" = "Adatbázis jelmondatának ellenőrzése"; +"Verify database passphrase" = "Az adatbázis jelmondatának hitelesítése"; /* No comment provided by engineer. */ -"Verify passphrase" = "Jelmondat ellenőrzése"; +"Verify passphrase" = "Jelmondat hitelesítése"; /* No comment provided by engineer. */ -"Verify security code" = "Biztonsági kód ellenőrzése"; +"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" = "kapcsolattartási azonosító-hivatkozáson keresztül"; +"via contact address link" = "a kapcsolattartási címhivatkozáson keresztül"; /* chat list item description */ -"via group link" = "csoport hivatkozáson keresztül"; +"via group link" = "a csoporthivatkozáson keresztül"; /* chat list item description */ -"via one-time link" = "egyszer használatos hivatkozáson keresztül"; +"via one-time link" = "egy egyszer használható meghívón keresztül"; /* No comment provided by engineer. */ -"via relay" = "átjátszón keresztül"; +"via relay" = "egy továbbítókiszolgálón keresztül"; /* No comment provided by engineer. */ -"Via secure quantum resistant protocol." = "Biztonságos kvantum ellenálló protokoll által."; +"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"; @@ -4122,11 +5563,17 @@ "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 1Gb méretig"; +"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"; @@ -4137,61 +5584,67 @@ "Voice messages" = "Hangüzenetek"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this chat." = "A hangüzenetek le vannak tiltva ebben a csevegésben."; +"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 in this group." = "A hangüzenetek küldése le van tiltva ebben a csoportban."; +"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 tilva!"; +"Voice messages prohibited!" = "A hangüzenetek le vannak tiltva!"; /* No comment provided by engineer. */ -"waiting for answer…" = "várakozás válaszra…"; +"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 az asztali kliensre..."; +"Waiting for desktop..." = "Várakozás a számítógép-alkalmazásra…"; /* No comment provided by engineer. */ -"Waiting for file" = "Fájlra várakozás"; +"Waiting for file" = "Várakozás a fájlra"; /* No comment provided by engineer. */ -"Waiting for image" = "Képre várakozás"; +"Waiting for image" = "Várakozás a képre"; /* No comment provided by engineer. */ -"Waiting for video" = "Videóra várakozás"; +"Waiting for video" = "Várakozás a videóra"; /* No comment provided by engineer. */ -"wants to connect to you!" = "kapcsolatba akar lépni önnel!"; +"Wallpaper accent" = "Háttérkép kiemelőszíne"; /* 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, továbbá üzenetkézbesítési hibákat okozhat"; +"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"; +"WebRTC ICE servers" = "WebRTC ICE-kiszolgálók"; /* time unit */ "weeks" = "hét"; /* No comment provided by engineer. */ -"Welcome %@!" = "Üdvözöllek %@!"; +"Welcome %@!" = "Üdvözöljük %@!"; /* No comment provided by engineer. */ -"Welcome message" = "Üdvözlő üzenet"; +"Welcome message" = "Üdvözlőüzenet"; /* No comment provided by engineer. */ -"Welcome message is too long" = "Az üdvözlő üzenet túl hosszú"; +"Welcome message is too long" = "Az üdvözlőüzenet túl hosszú"; /* No comment provided by engineer. */ -"What's new" = "Milyen újdonságok vannak"; +"What's new" = "Újdonságok"; /* No comment provided by engineer. */ "When available" = "Amikor elérhető"; @@ -4200,7 +5653,10 @@ "When connecting audio and video calls." = "Amikor egy bejövő hang- vagy videóhívás érkezik."; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Amikor az emberek kapcsolódást kérelmeznek, ön elfogadhatja vagy elutasíthatja azokat."; +"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."; @@ -4215,58 +5671,70 @@ "Wired ethernet" = "Vezetékes Ethernet"; /* No comment provided by engineer. */ -"With encrypted files and media." = "Titkosított fájlokkal és médiatartalommal."; +"With encrypted files and media." = "Titkosított fájlokkal és médiatartalmakkal."; /* No comment provided by engineer. */ -"With optional welcome message." = "Opcionális üdvözlő üzenettel."; +"With optional welcome message." = "Nem kötelező üdvözlőüzenettel."; /* No comment provided by engineer. */ -"With reduced battery usage." = "Csökkentett akkumulátorhasználattal."; +"With reduced battery usage." = "Csökkentett akkumulátor-használattal."; /* No comment provided by engineer. */ -"Wrong database passphrase" = "Téves adatbázis jelmondat"; +"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 passphrase!" = "Téves jelmondat!"; +"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. */ -"XFTP servers" = "XFTP kiszolgálók"; +"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" = "Ön"; +"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" = "Kapcsolódás elfogadva"; +"You accepted connection" = "Kapcsolat létrehozása"; /* No comment provided by engineer. */ -"You allow" = "Engedélyezte"; +"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ített névvel. Válasszon egy másik nevet."; +"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 %@." = "Már kapcsolódva van hozzá: %@."; +"You are already connected to %@." = "Ön már kapcsolódott a következőhöz: %@."; /* No comment provided by engineer. */ -"You are already connecting to %@." = "Már folyamatban van a kapcsolódás ehhez: %@."; +"You are already connected with %@." = "Ön már kapcsolódva van vele: %@."; /* 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álatos hivatkozáson keresztül!"; +"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 in group %@." = "Már a(z) %@ csoport tagja."; +"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 joining the group %@." = "A csatlakozás már folyamatban van a(z) %@ csoporthoz."; +"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!"; @@ -4275,154 +5743,178 @@ "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?" = "Csatlakozás folyamatban!\nCsatlakozási kérés megismétlése?"; +"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." = "Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál."; +"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" = "meghívást kapott a csoportba"; +"you are invited to group" = "Ön meghívást kapott a csoportba"; /* No comment provided by engineer. */ -"You are invited to group" = "Meghívást kapott a csoportba"; +"You are invited to group" = "Ön meghívást kapott a csoportba"; /* No comment provided by engineer. */ -"you are observer" = "megfigyelő szerep"; +"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 %@" = "blokkolta őt: %@"; +"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áshitelesítés nélkül."; +"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"; +"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 alkalmazás Adatvédelem és biztonság menüpontban."; +"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." = "Elrejthet vagy némíthat egy felhasználói profilt – csúsztasson jobbra."; +"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 SimpleX ismerősök számára a Beállításokban."; +"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 send messages to %@" = "Mostantól küldhet üzeneteket %@ számára"; +"You can now chat with %@" = "Mostantól küldhet üzeneteket %@ számára"; /* No comment provided by engineer. */ -"You can set lock screen notification preview via settings." = "A beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét."; +"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 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 csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait."; +"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 share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt az azonosítót az ismerőseivel, hogy kapcsolatba léphessenek önnel a **%@** nevű profilján keresztül."; +"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 your address as a link or QR code - anybody can connect to you." = "Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz."; +"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 start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás Beállítások / Adatbázis menü segítségével vagy az alkalmazás újraindításával indíthatja el"; +"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 turn on SimpleX Lock via Settings." = "A SimpleX zárolás a Beállításokon keresztül kapcsolható be."; +"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:"; -/* No comment provided by engineer. */ -"You can view invitation link again in connection details." = "A meghívó hivatkozást újra megtekintheti a kapcsolat részleteinél."; +/* 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" = "azonosítója megváltoztatva"; +"you changed address" = "Ön módosította a címet"; /* chat item text */ -"you changed address for %@" = "%@ azonosítója megváltoztatva"; +"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 %@" = "saját szerepkör megváltoztatva erre: %@"; +"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 %@" = "%1$@ szerepkörét megváltoztatta erre: %@"; +"you changed role of %@ to %@" = "Ön a következőre módosította %1$@ szerepkörét: „%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." = "Ön szabályozhatja, hogy mely kiszogál(ók)ón keresztül **kapja** az üzeneteket, az ismerősöket - az üzenetküldéshez használt szervereken."; +"You could not be verified; please try again." = "Nem sikerült hitelesíteni; próbálja meg újra."; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Nem lehetett ellenőrizni; próbálja meg újra."; +"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ért egy kapcsolódási kérelmet ezen az azonosítón keresztül!"; +"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?" = "Már kért egy kapcsolódási kérelmet!\nKapcsolódási kérés megismétlése?"; +"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 no chats" = "Nincsenek csevegési üzenetek"; +"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 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 kerül tárolásra."; +"You invited a contact" = "Ön meghívta egy partnerét"; /* No comment provided by engineer. */ -"You invited a contact" = "Meghívta egy ismerősét"; +"You joined this group" = "Ön csatlakozott ehhez a csoporthoz"; /* No comment provided by engineer. */ -"You joined this group" = "Csatlakozott ehhez a csoporthoz"; - -/* No comment provided by engineer. */ -"You joined this group. Connecting to inviting group member." = "Csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz."; +"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" = "elhagyta a csoportot"; +"you left" = "Ön elhagyta a csoportot"; /* 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 ismerőstől."; +"You may migrate the exported database." = "Az exportált adatbázist átköltöztetheti."; /* No comment provided by engineer. */ -"You need to allow your contact to send voice messages to be able to send them." = "Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősök számára."; +"You may save the exported archive." = "Az exportált archívumot elmentheti."; /* No comment provided by engineer. */ -"You rejected group invitation" = "Csoport meghívó elutasítva"; +"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 %@" = "eltávolította őt: %@"; +"you removed %@" = "Ön eltávolította őt: %@"; /* No comment provided by engineer. */ -"You sent group invitation" = "Csoport meghívó elküldve"; +"You sent group invitation" = "Csoportmeghívó elküldve"; /* chat list item description */ -"you shared one-time link" = "egyszer használatos hivatkozást osztott meg"; +"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" = "egyszer használatos hivatkozást osztott meg inkognitóban"; +"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 %@" = "feloldotta %@ blokkolását"; +"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 csoportos hivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!"; +"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 kapcsolódási kérelme elfogadásra kerül, várjon, vagy ellenőrizze később!"; +"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 le kapcsolódva, amikor az ismerőse eszköze online lesz, várjon, vagy ellenőrizze később!"; +"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 indításakor, vagy 30 másodpercnyi háttérben töltött idő után az alkalmazáshoz visszatérve hitelesítés szükséges."; +"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."; @@ -4430,23 +5922,23 @@ /* 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 az ismerőseit, ha később törli az azonosítóját."; +"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: "; +"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 ismerőst próbál meghívni, akivel inkognító profilt osztott meg abban a csoportban, amelyben saját fő profilja van használatban"; +"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" = "Inkognító profilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében meghívók küldése tiltott"; - -/* No comment provided by engineer. */ -"Your %@ servers" = "%@ nevű profiljához tartozó kiszolgálók"; +"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"; @@ -4457,29 +5949,35 @@ /* 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."; -/* No comment provided by engineer. */ -"Your chat profiles" = "Csevegési profiljai"; +/* alert title */ +"Your chat preferences" = "Az Ön csevegési beállításai"; /* 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)." = "Az ismerősnek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nMegszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással)."; +"Your chat profiles" = "Csevegési profilok"; /* No comment provided by engineer. */ -"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@)."; +"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 contacts can allow full message deletion." = "Ismerősök engedélyezhetik a teljes üzenet törlést."; +"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 will remain connected." = "Az ismerősei továbbra is kapcsolódva maradnak."; +"Your contacts can allow full message deletion." = "A partnerei engedélyezhetik a teljes üzenet törlését."; /* 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ÖRLŐDNI FOG, és a HELYÉRE az importált adatbázis kerül."; +"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" = "ICE kiszolgálók"; +"Your ICE servers" = "Saját ICE-kiszolgálók"; /* No comment provided by engineer. */ "Your preferences" = "Beállítások"; @@ -4491,32 +5989,29 @@ "Your profile" = "Profil"; /* No comment provided by engineer. */ -"Your profile **%@** will be shared." = "A(z) **%@** nevű profilja megosztásra fog kerülni."; +"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.\nSimpleX servers cannot see your profile." = "Profilja az eszközön van tárolva, és csak az ismerősökkel kerül megosztásra.\nA SimpleX kiszolgálók nem látjhatják profilját."; +"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." = "Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra."; +"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" = "Saját kiszolgáló"; +"Your server address" = "Saját SMP-kiszolgálójának címe"; /* No comment provided by engineer. */ -"Your server address" = "Saját kiszolgáló cím"; +"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" = "SimpleX azonosítója"; - -/* No comment provided by engineer. */ -"Your SMP servers" = "SMP kiszolgálók"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "XFTP kiszolgálók"; +"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 index 7b75cfcea3..f389e41458 100644 --- a/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings @@ -2,17 +2,17 @@ "CFBundleName" = "SimpleX"; /* Privacy - Camera Usage Description */ -"NSCameraUsageDescription" = "A SimpleX-nek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz."; +"NSCameraUsageDescription" = "A SimpleXnek 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őprofil használatát számítógépen keresztül ugyanazon a hálózaton."; +"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 SimpleX-nek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez."; +"NSMicrophoneUsageDescription" = "A SimpleXnek 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 SimpleX-nek hozzáférésre van szüksége a Galériához a rögzített és fogadott média mentéséhez"; +"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 e27b2e04a8..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)"; @@ -65,10 +35,7 @@ "[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 contact**: to create a new invitation link, or connect via a link you received." = "**Aggiungi contatto**: per creare un nuovo link di invito o connetterti tramite un link che hai ricevuto."; - -/* 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 group**: to create a new group." = "**Crea gruppo**: per creare un nuovo gruppo."; @@ -80,10 +47,10 @@ "**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. */ "**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."; @@ -92,7 +59,10 @@ "**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 / 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."; @@ -155,7 +125,10 @@ "%@ 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"; @@ -163,6 +136,9 @@ /* 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"; @@ -175,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"; @@ -187,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"; @@ -229,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"; @@ -277,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 giorno"; /* time interval */ @@ -286,12 +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"; @@ -326,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"; @@ -338,54 +338,107 @@ "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 contact" = "Aggiungi contatto"; +"Add friends" = "Aggiungi amici"; /* No comment provided by engineer. */ -"Add preset servers" = "Aggiungi server preimpostati"; +"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"; @@ -401,27 +454,42 @@ /* 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!"; @@ -431,6 +499,15 @@ /* 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."; @@ -446,9 +523,15 @@ /* 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 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)"; @@ -464,9 +547,15 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Permetti l'invio di messaggi a tempo."; +/* No comment provided by engineer. */ +"Allow sharing" = "Consenti la condivisione"; + /* No comment provided by engineer. */ "Allow 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."; @@ -509,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"; @@ -518,9 +610,15 @@ /* 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: %@"; @@ -530,6 +628,9 @@ /* 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"; @@ -539,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"; @@ -551,15 +655,48 @@ /* 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"; @@ -594,14 +731,20 @@ "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"; @@ -617,12 +760,39 @@ /* 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"; @@ -647,12 +817,19 @@ /* rcv group event chat item */ "blocked %@" = "ha bloccato %@"; -/* marked deleted chat item preview text */ +/* 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"; @@ -674,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!"; @@ -692,9 +884,18 @@ /* No comment provided by engineer. */ "Calls" = "Chiamate"; +/* No comment provided by engineer. */ +"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!"; @@ -702,6 +903,10 @@ "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. */ @@ -714,14 +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?"; @@ -747,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 */ @@ -757,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 %@…"; @@ -766,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"; @@ -777,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"; @@ -789,16 +1018,40 @@ /* 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. */ @@ -814,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. */ @@ -822,6 +1084,12 @@ /* 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?"; @@ -829,10 +1097,16 @@ "Clear verification" = "Annulla la verifica"; /* No comment provided by engineer. */ -"colored" = "colorato"; +"Color chats with the new themes." = "Colora le chat con i nuovi temi."; /* No comment provided by engineer. */ -"Colors" = "Colori"; +"Color mode" = "Modalità di colore"; + +/* No comment provided by engineer. */ +"colored" = "colorato"; + +/* report reason */ +"Community guidelines violation" = "Violazione delle linee guida della comunità"; /* server test step */ "Compare file" = "Confronta file"; @@ -843,15 +1117,48 @@ /* 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 files from unknown servers." = "Conferma i file da server sconosciuti."; + /* No comment provided by engineer. */ "Confirm network settings" = "Conferma le impostazioni di rete"; @@ -870,6 +1177,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Conferma caricamento"; +/* token status text */ +"Confirmed" = "Confermato"; + /* server test step */ "Connect" = "Connetti"; @@ -885,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?"; @@ -909,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)"; @@ -942,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"; @@ -960,18 +1291,39 @@ /* 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$@"; @@ -981,6 +1333,9 @@ /* 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"; @@ -994,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"; @@ -1002,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 %@?"; @@ -1024,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"; @@ -1041,6 +1411,9 @@ /* No comment provided by engineer. */ "Create link" = "Crea link"; +/* No comment provided by engineer. */ +"Create list" = "Crea elenco"; + /* 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/). 💻"; @@ -1059,15 +1432,15 @@ /* No comment provided by engineer. */ "Create your profile" = "Crea il tuo profilo"; +/* No comment provided by engineer. */ +"Created" = "Creato"; + /* No comment provided by engineer. */ "Created at" = "Creato il"; /* copied message info */ "Created at: %@" = "Creato il: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Creato il %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Creazione link dell'archivio"; @@ -1077,12 +1450,18 @@ /* 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 %@."; @@ -1092,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"; @@ -1155,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. */ @@ -1170,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?"; @@ -1192,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"; @@ -1203,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"; @@ -1210,10 +1612,7 @@ "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"; @@ -1251,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. */ @@ -1270,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?"; @@ -1281,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"; @@ -1299,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"; @@ -1320,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"; @@ -1348,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"; @@ -1362,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"; @@ -1372,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"; @@ -1398,30 +1848,61 @@ /* 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"; -/* chat item action */ +/* 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"; @@ -1434,12 +1915,18 @@ /* 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"; @@ -1452,12 +1939,15 @@ /* 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"; @@ -1491,6 +1981,9 @@ /* enabled status */ "enabled" = "attivato"; +/* No comment provided by engineer. */ +"Enabled" = "Attivato"; + /* No comment provided by engineer. */ "Enabled for" = "Attivo per"; @@ -1566,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"; @@ -1614,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"; @@ -1641,6 +2152,9 @@ /* 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"; @@ -1650,6 +2164,9 @@ /* 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"; @@ -1662,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"; @@ -1692,26 +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"; +/* alert title */ +"Error loading servers" = "Errore nel caricamento dei server"; + /* No comment provided by engineer. */ -"Error loading %@ servers" = "Errore nel caricamento dei server %@"; +"Error migrating settings" = "Errore nella migrazione delle impostazioni"; /* No comment provided by engineer. */ "Error opening chat" = "Errore di apertura della chat"; -/* No comment provided by engineer. */ +/* 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"; @@ -1725,6 +2260,9 @@ /* 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"; @@ -1753,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"; @@ -1779,7 +2326,9 @@ /* No comment provided by engineer. */ "Error: " = "Errore: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Errore: %@"; /* No comment provided by engineer. */ @@ -1789,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"; @@ -1800,12 +2352,21 @@ /* 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."; @@ -1821,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."; @@ -1839,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"; @@ -1846,7 +2440,7 @@ "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"; @@ -1887,15 +2481,45 @@ /* 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"; @@ -1905,6 +2529,24 @@ /* 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"; @@ -1917,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."; @@ -1929,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"; @@ -1968,27 +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. (24 hours)" = "I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore)"; - -/* 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 SimpleX links." = "I membri del gruppo possono inviare link di Simplex."; - -/* 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:"; @@ -2016,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"; @@ -2050,6 +2683,12 @@ "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. */ @@ -2095,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"; @@ -2109,9 +2748,15 @@ /* 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"; @@ -2130,6 +2775,15 @@ /* 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"; @@ -2184,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"; @@ -2223,7 +2895,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Risposta non valida"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Indirizzo del server non valido!"; /* item status text */ @@ -2235,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"; @@ -2262,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"; @@ -2269,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."; @@ -2280,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 (%@)."; @@ -2292,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. */ @@ -2319,13 +3003,16 @@ /* No comment provided by engineer. */ "Joining group" = "Ingresso nel gruppo"; -/* No comment provided by engineer. */ +/* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Tenere l'invito inutilizzato?"; /* No comment provided by engineer. */ @@ -2343,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"; @@ -2373,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"; @@ -2382,9 +3084,6 @@ /* No comment provided by engineer. */ "Live messages" = "Messaggi in diretta"; -/* No comment provided by engineer. */ -"Local" = "Locale"; - /* No comment provided by engineer. */ "Local name" = "Nome locale"; @@ -2397,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"; @@ -2433,6 +3123,12 @@ /* 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"; @@ -2440,28 +3136,85 @@ "Member" = "Membro"; /* profile update event chat item */ -"member %@ changed to %@" = "membro %1$@ cambiato in %2$@"; +"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"; @@ -2470,14 +3223,29 @@ "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"; @@ -2493,6 +3261,18 @@ /* 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."; @@ -2533,7 +3313,7 @@ "Migration is completed" = "La migrazione è completata"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrazioni: %@"; +"Migrations:" = "Migrazioni:"; /* time unit */ "minutes" = "minuti"; @@ -2556,31 +3336,40 @@ /* 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. */ @@ -2589,39 +3378,57 @@ /* 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"; @@ -2637,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"; @@ -2646,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"; @@ -2658,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"; @@ -2670,27 +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\")"; @@ -2698,11 +3586,11 @@ "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"; /* feature offered item */ @@ -2711,7 +3599,7 @@ /* feature offered item */ "offered %@: %@" = "offerto %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2720,9 +3608,6 @@ /* No comment provided by engineer. */ "Old database" = "Database vecchio"; -/* No comment provided by engineer. */ -"Old database archive" = "Vecchio archivio del database"; - /* group pref value */ "on" = "on"; @@ -2730,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."; @@ -2750,6 +3641,12 @@ /* 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."; @@ -2780,15 +3677,21 @@ /* 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"; @@ -2798,15 +3701,18 @@ /* No comment provided by engineer. */ "Open Settings" = "Apri le impostazioni"; -/* authentication reason */ -"Open user profiles" = "Apri i profili utente"; - -/* 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."; - /* No comment provided by engineer. */ "Opening app…" = "Apertura dell'app…"; +/* No comment provided by engineer. */ +"Operator" = "Operatore"; + +/* alert title */ +"Operator server" = "Server dell'operatore"; + +/* No comment provided by engineer. */ +"Or import archive file" = "O importa file archivio"; + /* No comment provided by engineer. */ "Or paste archive link" = "O incolla il link dell'archivio"; @@ -2819,9 +3725,24 @@ /* 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"; @@ -2843,6 +3764,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "Codice di accesso impostato!"; +/* No comment provided by engineer. */ +"Password" = "Password"; + /* No comment provided by engineer. */ "Password to show" = "Password per mostrare"; @@ -2865,10 +3789,16 @@ "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"; @@ -2882,9 +3812,18 @@ /* 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."; @@ -2924,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"; @@ -2934,26 +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"; @@ -2963,16 +3941,13 @@ /* No comment provided by engineer. */ "Profile images" = "Immagini del profilo"; -/* No comment provided by engineer. */ -"Profile name" = "Nome del profilo"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nome 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. */ @@ -2987,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."; @@ -3005,15 +3983,30 @@ /* 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"; @@ -3029,20 +4022,23 @@ /* 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/chat-profiles.html#incognito-mode)." = "Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](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)."; @@ -3051,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…"; @@ -3075,10 +4071,16 @@ "Received message" = "Messaggio 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."; +"Received messages" = "Messaggi ricevuti"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Ricezione concomitanza"; +"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."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "La ricezione del file verrà interrotta."; @@ -3095,9 +4097,24 @@ /* 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?"; @@ -3110,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. */ @@ -3119,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"; @@ -3131,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"; @@ -3182,18 +4218,66 @@ /* 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"; @@ -3219,7 +4303,7 @@ "Reveal" = "Rivela"; /* No comment provided by engineer. */ -"Revert" = "Ripristina"; +"Review conditions" = "Leggi le condizioni"; /* No comment provided by engineer. */ "Revoke" = "Revoca"; @@ -3236,40 +4320,44 @@ /* No comment provided by engineer. */ "Run chat" = "Avvia chat"; +/* No comment provided by engineer. */ +"Safely receive files" = "Ricevi i file in sicurezza"; + /* No comment provided by engineer. */ "Safer groups" = "Gruppi più sicuri"; -/* chat item action */ +/* 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. */ @@ -3278,15 +4366,15 @@ /* 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"; @@ -3305,6 +4393,15 @@ /* 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"; @@ -3320,6 +4417,9 @@ /* 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"; @@ -3332,6 +4432,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secondario"; + /* time unit */ "seconds" = "secondi"; @@ -3341,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"; @@ -3350,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"; @@ -3377,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"; @@ -3387,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"; @@ -3410,7 +4534,7 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Invia fino a 100 ultimi messaggi ai nuovi membri."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Il mittente ha annullato il trasferimento del file."; /* No comment provided by engineer. */ @@ -3446,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"; @@ -3464,29 +4630,53 @@ /* 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" = "impostata nuova immagine del profilo"; +"set new profile picture" = "ha impostato una nuova immagine del profilo"; /* No comment provided by engineer. */ "Set passcode" = "Imposta codice"; @@ -3506,30 +4696,58 @@ /* No comment provided by engineer. */ "Settings" = "Impostazioni"; +/* alert message */ +"Settings were changed." = "Le impostazioni sono state cambiate."; + /* No comment provided by engineer. */ "Shape profile images" = "Forma delle immagini del profilo"; -/* chat item action */ +/* 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 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"; @@ -3539,6 +4757,12 @@ /* 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"; @@ -3548,12 +4772,27 @@ /* 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."; @@ -3570,7 +4809,7 @@ "SimpleX links" = "Link di SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "I link di SimpleX sono vietati in questo gruppo."; +"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"; @@ -3590,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"; @@ -3603,14 +4848,36 @@ "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. */ "Square, circle, or anything in between." = "Quadrata, circolare o qualsiasi forma tra le due."; @@ -3626,18 +4893,21 @@ /* 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" = "Ferma la chat"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Ferma la chat per attivare le azioni del database"; - /* 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."; @@ -3653,10 +4923,10 @@ /* 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 */ @@ -3665,27 +4935,54 @@ /* 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."; @@ -3705,11 +5002,14 @@ "Tap to scan" = "Tocca per scansionare"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Tocca per iniziare una chat"; +"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"; @@ -3719,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. */ @@ -3741,10 +5047,13 @@ "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."; +"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 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."; +"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."; @@ -3752,6 +5061,9 @@ /* 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!"; @@ -3764,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."; @@ -3777,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! ✅"; @@ -3794,11 +5118,20 @@ /* No comment provided by engineer. */ "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. */ +"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. */ "The text you pasted is not a SimpleX link." = "Il testo che hai incollato non è un link SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Tema"; +"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 **%@**."; @@ -3812,6 +5145,9 @@ /* 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."; @@ -3842,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:"; @@ -3858,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."; @@ -3866,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: %@)."; @@ -3920,13 +5304,13 @@ /* rcv group event chat item */ "unblocked %@" = "ha sbloccato %@"; -/* item status description */ -"Unexpected error: %@" = "Errore imprevisto: % @"; +/* 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. */ @@ -3953,6 +5337,12 @@ /* 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"; @@ -3974,21 +5364,24 @@ /* 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. */ +"Unsupported connection link" = "Link di connessione non supportato"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Vengono inviati ai nuovi membri fino a 100 ultimi messaggi."; /* No comment provided by engineer. */ "Update" = "Aggiorna"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Aggiornare l'impostazione degli host .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Aggiorna la password del database"; @@ -3996,7 +5389,10 @@ "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" = "ha aggiornato il profilo del gruppo"; @@ -4008,10 +5404,10 @@ "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"; @@ -4019,18 +5415,33 @@ /* 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"; @@ -4046,20 +5457,47 @@ /* 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. */ +"Use SOCKS proxy" = "Usa proxy SOCKS"; + +/* No comment provided by engineer. */ +"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. */ -"User profile" = "Profilo utente"; +"Use the app with one hand." = "Usa l'app con una mano sola."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "L'uso di host .onion richiede un fornitore di VPN compatibile."; +"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."; @@ -4109,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"; @@ -4124,9 +5565,15 @@ /* 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"; @@ -4140,7 +5587,7 @@ "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"; @@ -4166,6 +5613,12 @@ /* 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!"; @@ -4200,7 +5653,10 @@ "When connecting audio and video calls." = "Quando si connettono le chiamate audio e video."; /* 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 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."; @@ -4223,14 +5679,26 @@ /* 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ì"; @@ -4238,9 +5706,6 @@ /* No comment provided by engineer. */ "you" = "tu"; -/* No comment provided by engineer. */ -"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."; @@ -4256,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 %@."; @@ -4286,6 +5754,9 @@ /* 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"; @@ -4295,6 +5766,12 @@ /* 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 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"; @@ -4314,7 +5791,13 @@ "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."; @@ -4326,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."; @@ -4337,7 +5820,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Puoi usare il markdown per formattare i messaggi:"; -/* No comment provided by engineer. */ +/* 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. */ @@ -4356,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." = "Tu decidi attraverso quale/i server **ricevere** i messaggi, i tuoi contatti quali server 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!"; @@ -4367,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."; @@ -4385,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."; @@ -4406,6 +5895,9 @@ /* 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 %@"; @@ -4430,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."; @@ -4445,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"; @@ -4457,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 (%@)."; @@ -4472,6 +5967,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "I tuoi contatti resteranno connessi."; +/* No comment provided by engineer. */ +"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."; @@ -4494,7 +5992,10 @@ "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."; @@ -4503,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"; @@ -4514,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 f8bacda7b8..d214f88e1c 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1,54 +1,30 @@ -/* 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. */ -"." = "."; - -/* 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)"; @@ -59,10 +35,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[GitHub でスターを付ける](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。"; - -/* 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 group**: to create a new group." = "**グループ作成**: 新しいグループを作成する。"; @@ -74,10 +47,10 @@ "**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. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**注意**: 2つの端末で同じデータベースを使用すると、セキュリティ保護として、あなたが接続しているメッセージの復号化が解除されます。"; @@ -86,7 +59,10 @@ "**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 / Paste link**: to connect via a link you received." = "**QRスキャン / リンクの貼り付け**: 受け取ったリンクで接続する。"; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。"; @@ -113,7 +89,7 @@ "%@" = "%@"; /* No comment provided by engineer. */ -"%@ (current)" = "%@ (現在)"; +"%@ (current)" = "%@ (現在)"; /* copied message info */ "%@ (current):" = "%@ (現在):"; @@ -148,6 +124,9 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ は検証されています"; +/* No comment provided by engineer. */ +"%@ server" = "%@ サーバー"; + /* No comment provided by engineer. */ "%@ servers" = "%@ サーバー"; @@ -157,6 +136,9 @@ /* notification title */ "%@ wants to connect!" = "%@ が接続を希望しています!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@や%@など%lld人のメンバー"; @@ -169,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 分"; @@ -188,7 +185,7 @@ "%d weeks" = "%d 週"; /* No comment provided by engineer. */ -"%lld" = "%lld"; +"%lld" = ""; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; @@ -223,9 +220,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 秒"; @@ -271,7 +265,8 @@ /* No comment provided by engineer. */ "0s" = "0秒"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1日"; /* time interval */ @@ -280,12 +275,20 @@ /* 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" = "使い捨てリンク"; + +/* 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分"; @@ -319,23 +322,15 @@ /* 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. */ @@ -344,7 +339,8 @@ /* notification body */ "Accept contact request from %@?" = "%@ からの連絡要求を受け入れますか?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "シークレットモードで承諾"; /* call status */ @@ -353,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コードでサーバを追加する。"; @@ -371,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" = "アドレス"; @@ -386,6 +385,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "ネットワーク詳細設定"; +/* No comment provided by engineer. */ +"Advanced settings" = "詳細設定"; + /* chat item text */ "agreeing encryption for %@…" = "%@の暗号化に同意しています…"; @@ -407,6 +409,9 @@ /* 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." = "あなたの連絡先が繋がったまま継続します。"; @@ -426,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. (24 hours)" = "送信相手も永久メッセージ削除を許可する時のみに許可する。"; +"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." = "連絡先が許可している場合にのみ、メッセージへのリアクションを許可します。"; @@ -440,6 +445,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "消えるメッセージの送信を許可する。"; +/* No comment provided by engineer. */ +"Allow sharing" = "共有を許可"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "送信済みメッセージの永久削除を許可する。(24時間)"; @@ -485,6 +493,9 @@ /* pref value */ "always" = "常に"; +/* No comment provided by engineer. */ +"Always use private routing." = "プライベートルーティングを常に使用する。"; + /* No comment provided by engineer. */ "Always use relay" = "常にリレーを経由する"; @@ -494,6 +505,9 @@ /* No comment provided by engineer. */ "Answer call" = "通話に応答"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; + /* No comment provided by engineer. */ "App build: %@" = "アプリのビルド: %@"; @@ -519,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" = "添付する"; @@ -623,7 +646,8 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "連絡先を招待できません!"; -/* No comment provided by engineer. */ +/* alert action +alert button */ "Cancel" = "中止"; /* feature offered item */ @@ -632,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. */ @@ -663,7 +687,7 @@ "Change self-destruct mode" = "自己破壊モードの変更"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "自己破壊パスコードを変更する"; /* chat item text */ @@ -681,9 +705,6 @@ /* chat item text */ "changing address…" = "アドレスを変更しています…"; -/* No comment provided by engineer. */ -"Chat archive" = "チャットのアーカイブ"; - /* No comment provided by engineer. */ "Chat console" = "チャットのコンソール"; @@ -706,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. */ @@ -721,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. */ @@ -729,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" = "ファイルを比較"; @@ -747,6 +783,9 @@ /* No comment provided by engineer. */ "complete" = "完了"; +/* No comment provided by engineer. */ +"Completed" = "完了"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICEサーバを設定"; @@ -771,9 +810,15 @@ /* 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" = "リンク経由で接続"; @@ -783,9 +828,24 @@ /* 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)" = "接続待ち (承諾済み)"; @@ -807,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" = "接続エラー"; @@ -825,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" = "接続タイムアウト"; @@ -849,9 +921,6 @@ /* notification */ "Contact is connected" = "連絡先は接続中"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "連絡先がまだ繋がってません!"; - /* No comment provided by engineer. */ "Contact name" = "連絡先の名前"; @@ -867,7 +936,7 @@ /* No comment provided by engineer. */ "Continue" = "続ける"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "コピー"; /* No comment provided by engineer. */ @@ -876,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" = "ファイルを作成"; @@ -891,6 +957,9 @@ /* 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 profile" = "プロフィールを作成する"; + /* server test step */ "Create queue" = "キューの作成"; @@ -903,9 +972,6 @@ /* No comment provided by engineer. */ "Create your profile" = "プロフィールを作成する"; -/* No comment provided by engineer. */ -"Created on %@" = "%@ によって作成されました"; - /* No comment provided by engineer. */ "creator" = "作成者"; @@ -924,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" = "データーベースのダウングレード"; @@ -987,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. */ @@ -1002,7 +1078,8 @@ /* No comment provided by engineer. */ "default (yes)" = "デフォルト(はい)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "削除"; /* No comment provided by engineer. */ @@ -1017,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" = "チャットのプロフィールを削除する"; @@ -1035,9 +1106,6 @@ /* No comment provided by engineer. */ "Delete contact" = "連絡先を削除"; -/* No comment provided by engineer. */ -"Delete Contact" = "連絡先を削除"; - /* No comment provided by engineer. */ "Delete database" = "データベースを削除"; @@ -1077,7 +1145,7 @@ /* No comment provided by engineer. */ "Delete message?" = "メッセージを削除しますか?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "メッセージを削除"; /* No comment provided by engineer. */ @@ -1089,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?" = "接続待ちの接続を削除しますか?"; @@ -1128,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" = "開発ツール"; @@ -1156,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)" = "無効にする(設定の優先を維持)"; @@ -1180,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" = "に消えます"; @@ -1201,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" = "有効にしない"; @@ -1239,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. */ @@ -1374,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)" = "メンバー追加にエラー発生"; @@ -1416,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" = "データベースの削除にエラー発生"; @@ -1446,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" = "グループのプロフィール保存にエラー発生"; @@ -1488,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. */ @@ -1509,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. */ @@ -1521,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" = "保存せずに閉じる"; @@ -1545,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. */ @@ -1567,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!" = "ファイルとメディアは禁止されています!"; @@ -1611,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!" = "完全に再実装されました - バックグラウンドで動作します!"; @@ -1653,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. (24 hours)" = "グループのメンバーがメッセージを完全削除することができます。(24時間)"; - -/* 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:" = "グループメッセージ:"; @@ -1728,9 +1765,6 @@ /* time unit */ "hours" = "時間"; -/* No comment provided by engineer. */ -"How it works" = "技術の説明"; - /* No comment provided by engineer. */ "How SimpleX works" = "SimpleX の仕組み"; @@ -1771,7 +1805,7 @@ "Immediately" = "即座に"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "スパムや悪質送信を防止"; +"Immune to spam" = "スパムや悪質送信を防止"; /* No comment provided by engineer. */ "Import" = "読み込む"; @@ -1840,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" = "インターフェース"; @@ -1860,7 +1894,7 @@ /* invalid chat item */ "invalid data" = "無効なデータ"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "無効なサーバアドレス!"; /* item status text */ @@ -1906,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." = "これにより単一のチャット プロファイル内で、データを共有せずに多数の匿名の接続をすることができます。"; @@ -1929,7 +1963,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "日本語UI"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "参加"; /* No comment provided by engineer. */ @@ -1959,7 +1993,7 @@ /* No comment provided by engineer. */ "Learn more" = "さらに詳しく"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "脱退"; /* No comment provided by engineer. */ @@ -1989,9 +2023,6 @@ /* No comment provided by engineer. */ "Live messages" = "ライブメッセージ"; -/* No comment provided by engineer. */ -"Local" = "自分のみ"; - /* No comment provided by engineer. */ "Local name" = "ローカルネーム"; @@ -2004,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" = "全員に対して削除済みマークを付ける"; @@ -2058,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" = "メッセージ送信エラー"; @@ -2071,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" = "メッセージを受信"; @@ -2085,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…" = "データベースのアーカイブを移行しています…"; @@ -2098,7 +2147,7 @@ "Migration is completed" = "移行が完了しました"; /* No comment provided by engineer. */ -"Migrations: %@" = "移行: %@"; +"Migrations:" = "移行:"; /* time unit */ "minutes" = "分"; @@ -2130,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. */ @@ -2154,7 +2200,7 @@ /* No comment provided by engineer. */ "Network status" = "ネットワーク状況"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "一度も"; /* notification */ @@ -2163,9 +2209,6 @@ /* notification */ "New contact:" = "新しい連絡先:"; -/* No comment provided by engineer. */ -"New database archive" = "新しいデータベースのアーカイブ"; - /* No comment provided by engineer. */ "New desktop app!" = "新しいデスクトップアプリ!"; @@ -2226,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" = "通知"; @@ -2245,11 +2294,11 @@ "observer" = "オブザーバー"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "オフ"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "オフ"; /* feature offered item */ @@ -2258,15 +2307,12 @@ /* 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" = "オン"; @@ -2274,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." = "グループ設定を変えられるのはグループのオーナーだけです。"; @@ -2324,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. */ @@ -2336,12 +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." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; - /* member role */ "owner" = "オーナー"; @@ -2370,10 +2410,7 @@ "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" = "永続的な復号化エラー"; @@ -2429,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" = "プレセットサーバのアドレス"; @@ -2447,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" = "プロフィールとサーバ接続"; @@ -2456,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. */ @@ -2504,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…" = "回答を受け取りました…"; @@ -2567,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. */ @@ -2651,9 +2686,6 @@ /* chat item action */ "Reveal" = "開示する"; -/* No comment provided by engineer. */ -"Revert" = "元に戻す"; - /* No comment provided by engineer. */ "Revoke" = "取り消す"; @@ -2669,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. */ @@ -2684,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" = "グループプロフィールの保存"; @@ -2699,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. */ @@ -2708,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?" = "ウェルカムメッセージを保存しますか?"; @@ -2756,7 +2780,7 @@ /* chat item text */ "security code changed" = "セキュリティコードが変更されました"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "選択"; /* No comment provided by engineer. */ @@ -2777,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" = "ダイレクトメッセージを送信して接続する"; @@ -2795,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. */ @@ -2870,7 +2888,8 @@ /* No comment provided by engineer. */ "Settings" = "設定"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "共有する"; /* No comment provided by engineer. */ @@ -2879,7 +2898,7 @@ /* No comment provided by engineer. */ "Share address" = "アドレスを共有する"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "アドレスを連絡先と共有しますか?"; /* No comment provided by engineer. */ @@ -2951,9 +2970,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." = "インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。"; @@ -2972,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." = "データベースのエクスポート、読み込み、削除するにはチャットを閉じてからです。チャットを閉じると送受信ができなくなります。"; @@ -2990,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 */ @@ -3029,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接続タイムアウト"; @@ -3053,7 +3063,7 @@ /* No comment provided by engineer. */ "Test servers" = "テストサーバ"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "テストは失敗しました!"; /* No comment provided by engineer. */ @@ -3065,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." = "アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。"; @@ -3086,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." = "以前のメッセージとハッシュ値が異なります。"; @@ -3098,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! ✅" = "長らくお待たせしました! ✅"; @@ -3116,9 +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. */ "These settings are for your current profile **%@**." = "これらの設定は現在のプロファイル **%@** 用です。"; @@ -3152,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." = "音声メッセージを録音する場合は、マイクの使用を許可してください。"; @@ -3191,13 +3195,10 @@ /* 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. */ @@ -3236,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" = "アップグレードしてチャットを開く"; @@ -3296,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 チャット サーバーを使用する。"; @@ -3357,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!" = "音声メッセージは使用禁止です!"; @@ -3401,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." = "連絡相手にシークレットモードのプロフィールを共有すると、その連絡相手に招待されたグループでも同じプロフィールが使われます。"; @@ -3416,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" = "接続を承認しました"; @@ -3464,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." = "設定からロック画面の通知プレビューを設定できます。"; @@ -3476,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" = "アプリの設定/データベースから、またはアプリを再起動することでチャットを開始できます"; @@ -3503,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." = "アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。"; @@ -3578,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" = "あなたの通話"; @@ -3593,9 +3564,6 @@ /* 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 (%@)." = "連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。"; @@ -3624,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." = "あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。"; @@ -3632,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" = "あなたのサーバアドレス"; @@ -3644,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/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index e12b8058f6..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,7 @@ "[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 contact**: to create a new invitation link, or connect via a link you received." = "**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. */ -"**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 group**: to create a new group." = "**Groep aanmaken**: om een nieuwe groep aan te maken."; @@ -80,10 +47,10 @@ "**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. */ "**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."; @@ -92,7 +59,10 @@ "**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 / 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."; @@ -154,6 +124,9 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ is geverifieerd"; +/* No comment provided by engineer. */ +"%@ server" = "%@ server"; + /* No comment provided by engineer. */ "%@ servers" = "%@ servers"; @@ -163,6 +136,9 @@ /* 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"; @@ -175,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"; @@ -187,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)"; @@ -229,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"; @@ -277,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 dag"; /* time interval */ @@ -286,12 +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"; @@ -311,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."; @@ -326,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"; @@ -338,47 +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 contact" = "Contact toevoegen"; +"Add friends" = "Vrienden toevoegen"; /* No comment provided by engineer. */ -"Add preset servers" = "Vooraf ingestelde servers toevoegen"; +"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"; @@ -386,6 +433,12 @@ /* 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"; @@ -401,27 +454,42 @@ /* 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!"; @@ -431,6 +499,12 @@ /* 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."; @@ -446,17 +520,23 @@ /* 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. (24 hours)" = "Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur)"; +"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."; @@ -465,7 +545,13 @@ "Allow sending disappearing messages." = "Toestaan dat verdwijnende berichten worden verzonden."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages. (24 hours)" = "Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur)"; +"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."; @@ -483,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. (24 hours)" = "Laat uw contacten verzonden berichten onomkeerbaar verwijderen. (24 uur)"; +"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."; @@ -509,6 +595,9 @@ /* pref value */ "always" = "altijd"; +/* No comment provided by engineer. */ +"Always use private routing." = "Gebruik altijd privéroutering."; + /* No comment provided by engineer. */ "Always use relay" = "Altijd relay gebruiken"; @@ -518,9 +607,15 @@ /* 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: %@"; @@ -530,6 +625,9 @@ /* 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"; @@ -539,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"; @@ -551,15 +652,48 @@ /* 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"; @@ -573,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"; @@ -599,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"; @@ -617,12 +757,39 @@ /* 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"; @@ -645,19 +812,26 @@ "blocked" = "geblokkeerd"; /* rcv group event chat item */ -"blocked %@" = "geblokkeerd %@"; +"blocked %@" = "blokkeerde %@"; -/* marked deleted chat item preview text */ +/* 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. (24 hours)" = "Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; @@ -675,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!"; @@ -692,9 +881,18 @@ /* No comment provided by engineer. */ "Calls" = "Oproepen"; +/* No comment provided by engineer. */ +"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!"; @@ -702,6 +900,10 @@ "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. */ @@ -714,14 +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?"; @@ -747,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 */ @@ -766,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"; @@ -777,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"; @@ -789,16 +1015,40 @@ /* 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. */ @@ -814,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. */ @@ -822,6 +1081,12 @@ /* 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?"; @@ -829,10 +1094,16 @@ "Clear verification" = "Verwijderd verificatie"; /* No comment provided by engineer. */ -"colored" = "gekleurd"; +"Color chats with the new themes." = "Kleurchats met de nieuwe thema's."; /* No comment provided by engineer. */ -"Colors" = "Kleuren"; +"Color mode" = "Kleur mode"; + +/* No comment provided by engineer. */ +"colored" = "gekleurd"; + +/* report reason */ +"Community guidelines violation" = "Schending van de communityrichtlijnen"; /* server test step */ "Compare file" = "Bestand vergelijken"; @@ -843,15 +1114,48 @@ /* 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"; @@ -870,6 +1174,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Bevestig het uploaden"; +/* token status text */ +"Confirmed" = "Bevestigd"; + /* server test step */ "Connect" = "Verbind"; @@ -885,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?"; @@ -909,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)"; @@ -942,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"; @@ -960,18 +1288,39 @@ /* 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$@"; @@ -981,6 +1330,9 @@ /* 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"; @@ -994,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"; @@ -1002,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 %@?"; @@ -1024,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"; @@ -1041,6 +1408,9 @@ /* No comment provided by engineer. */ "Create link" = "Maak link"; +/* No comment provided by engineer. */ +"Create list" = "Maak een lijst"; + /* 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/). 💻"; @@ -1059,15 +1429,15 @@ /* No comment provided by engineer. */ "Create your profile" = "Maak je profiel aan"; +/* No comment provided by engineer. */ +"Created" = "Gemaakt"; + /* No comment provided by engineer. */ "Created at" = "Gemaakt op"; /* copied message info */ "Created at: %@" = "Aangemaakt op: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Gemaakt op %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Archief link maken"; @@ -1077,12 +1447,18 @@ /* 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 %@."; @@ -1092,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"; @@ -1135,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"; @@ -1155,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. */ @@ -1170,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?"; @@ -1192,16 +1588,19 @@ "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"; @@ -1210,10 +1609,7 @@ "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"; @@ -1228,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"; @@ -1251,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. */ @@ -1270,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?"; @@ -1281,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"; @@ -1299,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"; @@ -1320,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"; @@ -1348,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"; @@ -1362,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"; @@ -1369,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"; @@ -1398,30 +1845,61 @@ /* 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"; -/* chat item action */ +/* 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"; @@ -1434,12 +1912,18 @@ /* 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"; @@ -1452,12 +1936,15 @@ /* 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"; @@ -1491,6 +1978,9 @@ /* enabled status */ "enabled" = "ingeschakeld"; +/* No comment provided by engineer. */ +"Enabled" = "Ingeschakeld"; + /* No comment provided by engineer. */ "Enabled for" = "Ingeschakeld voor"; @@ -1566,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"; @@ -1597,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…"; @@ -1614,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"; @@ -1641,6 +2149,9 @@ /* 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"; @@ -1650,6 +2161,9 @@ /* 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"; @@ -1662,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"; @@ -1692,26 +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"; +/* alert title */ +"Error loading servers" = "Fout bij het laden van servers"; + /* No comment provided by engineer. */ -"Error loading %@ servers" = "Fout bij het laden van %@ servers"; +"Error migrating settings" = "Fout bij migreren van instellingen"; /* No comment provided by engineer. */ "Error opening chat" = "Fout bij het openen van de chat"; -/* No comment provided by engineer. */ +/* 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"; @@ -1725,6 +2257,9 @@ /* 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"; @@ -1753,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"; @@ -1779,7 +2323,9 @@ /* No comment provided by engineer. */ "Error: " = "Fout: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Fout: %@"; /* No comment provided by engineer. */ @@ -1789,10 +2335,13 @@ "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"; @@ -1800,12 +2349,21 @@ /* chat item action */ "Expand" = "Uitklappen"; +/* No comment provided by engineer. */ +"expired" = "verlopen"; + +/* token status text */ +"Expired" = "Verlopen"; + /* No comment provided by engineer. */ "Export database" = "Database exporteren"; /* 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."; @@ -1821,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."; @@ -1839,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"; @@ -1846,13 +2437,13 @@ "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 not allowed" = "Bestanden en media niet toegestaan"; /* No comment provided by engineer. */ -"Files and media prohibited!" = "Bestanden en media verboden!"; +"Files and media prohibited!" = "Bestanden en media niet toegestaan!"; /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Filter ongelezen en favoriete chats."; @@ -1867,7 +2458,7 @@ "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"; @@ -1887,15 +2478,45 @@ /* 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"; @@ -1905,6 +2526,24 @@ /* 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"; @@ -1917,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."; @@ -1929,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"; @@ -1968,27 +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. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; - -/* 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 SimpleX links." = "Groepsleden kunnen SimpleX-links verzenden."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Groepsleden kunnen spraak berichten verzenden."; - /* notification */ "Group message:" = "Groep bericht:"; @@ -2008,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!"; @@ -2016,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"; @@ -2050,6 +2680,12 @@ "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. */ @@ -2074,7 +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 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:"; @@ -2095,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"; @@ -2109,9 +2745,15 @@ /* 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"; @@ -2130,6 +2772,15 @@ /* 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"; @@ -2184,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" = "Direct"; +"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"; @@ -2223,7 +2892,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Ongeldig antwoord"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Ongeldig server adres!"; /* item status text */ @@ -2235,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"; @@ -2262,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."; @@ -2280,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 (%@)."; @@ -2292,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 %@"; @@ -2319,13 +3000,16 @@ /* No comment provided by engineer. */ "Joining group" = "Deel nemen aan groep"; -/* No comment provided by engineer. */ +/* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Ongebruikte uitnodiging bewaren?"; /* No comment provided by engineer. */ @@ -2343,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"; @@ -2373,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"; @@ -2382,9 +3081,6 @@ /* No comment provided by engineer. */ "Live messages" = "Live berichten"; -/* No comment provided by engineer. */ -"Local" = "Lokaal"; - /* No comment provided by engineer. */ "Local name" = "Lokale naam"; @@ -2397,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"; @@ -2433,6 +3120,12 @@ /* 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"; @@ -2445,39 +3138,111 @@ /* 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"; @@ -2493,6 +3258,18 @@ /* 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."; @@ -2527,13 +3304,13 @@ "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"; @@ -2556,31 +3333,40 @@ /* 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. */ @@ -2589,39 +3375,57 @@ /* 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"; @@ -2637,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"; @@ -2646,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"; @@ -2658,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!"; @@ -2670,27 +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)"; @@ -2698,11 +3583,11 @@ "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"; /* feature offered item */ @@ -2711,7 +3596,7 @@ /* feature offered item */ "offered %@: %@" = "voorgesteld %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "OK"; /* No comment provided by engineer. */ @@ -2720,9 +3605,6 @@ /* No comment provided by engineer. */ "Old database" = "Oude database"; -/* No comment provided by engineer. */ -"Old database archive" = "Oud database archief"; - /* group pref value */ "on" = "aan"; @@ -2730,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."; @@ -2751,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). (24 hours)" = "Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering). (24 uur)"; +"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."; @@ -2766,7 +3660,7 @@ "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). (24 hours)" = "Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur)"; @@ -2780,15 +3674,21 @@ /* 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 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"; @@ -2798,15 +3698,18 @@ /* No comment provided by engineer. */ "Open Settings" = "Open instellingen"; -/* authentication reason */ -"Open user profiles" = "Gebruikers profielen 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."; - /* No comment provided by engineer. */ "Opening app…" = "App openen…"; +/* No comment provided by engineer. */ +"Operator" = "Operator"; + +/* alert title */ +"Operator server" = "Operatorserver"; + +/* No comment provided by engineer. */ +"Or import archive file" = "Of importeer archiefbestand"; + /* No comment provided by engineer. */ "Or paste archive link" = "Of plak de archief link"; @@ -2819,9 +3722,24 @@ /* 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"; @@ -2843,6 +3761,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "Toegangscode ingesteld!"; +/* No comment provided by engineer. */ +"Password" = "Wachtwoord"; + /* No comment provided by engineer. */ "Password to show" = "Wachtwoord om weer te geven"; @@ -2865,10 +3786,16 @@ "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"; @@ -2882,9 +3809,18 @@ /* 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."; @@ -2919,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"; @@ -2934,26 +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"; @@ -2963,30 +3938,30 @@ /* No comment provided by engineer. */ "Profile images" = "Profiel afbeeldingen"; -/* No comment provided by engineer. */ -"Profile name" = "Profielnaam"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profielnaam:"; - /* 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."; @@ -3006,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"; @@ -3014,6 +3995,15 @@ /* 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"; @@ -3029,20 +4019,23 @@ /* 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/chat-profiles.html#incognito-mode)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](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)."; @@ -3051,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…"; @@ -3075,10 +4068,16 @@ "Received message" = "Ontvangen bericht"; /* 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."; +"Received messages" = "Ontvangen berichten"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Gelijktijdig ontvangen"; +"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."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Het ontvangen van het bestand wordt gestopt."; @@ -3095,9 +4094,24 @@ /* 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?"; @@ -3110,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. */ @@ -3119,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"; @@ -3131,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"; @@ -3182,20 +4215,68 @@ /* 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"; @@ -3219,7 +4300,7 @@ "Reveal" = "Onthullen"; /* No comment provided by engineer. */ -"Revert" = "Terugdraaien"; +"Review conditions" = "Voorwaarden bekijken"; /* No comment provided by engineer. */ "Revoke" = "Intrekken"; @@ -3236,56 +4317,60 @@ /* No comment provided by engineer. */ "Run chat" = "Chat uitvoeren"; +/* No comment provided by engineer. */ +"Safely receive files" = "Veilig bestanden ontvangen"; + /* No comment provided by engineer. */ "Safer groups" = "Veiligere groepen"; -/* chat item action */ +/* 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?"; -/* No comment provided by engineer. */ -"Save welcome message?" = "Welkomst bericht opslaan?"; +/* alert title */ +"Save your profile?" = "Uw profiel opslaan?"; /* No comment provided by engineer. */ "saved" = "opgeslagen"; @@ -3305,6 +4390,15 @@ /* 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"; @@ -3320,6 +4414,9 @@ /* 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"; @@ -3332,6 +4429,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secundair"; + /* time unit */ "seconds" = "seconden"; @@ -3341,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"; @@ -3350,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"; @@ -3377,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"; @@ -3410,7 +4531,7 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Stuur tot 100 laatste berichten naar nieuwe leden."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Afzender heeft bestandsoverdracht geannuleerd."; /* No comment provided by engineer. */ @@ -3446,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"; @@ -3464,29 +4627,53 @@ /* 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 instellen"; +"set new profile picture" = "nieuwe profielfoto"; /* No comment provided by engineer. */ "Set passcode" = "Toegangscode instellen"; @@ -3506,30 +4693,55 @@ /* No comment provided by engineer. */ "Settings" = "Instellingen"; +/* alert message */ +"Settings were changed." = "Instellingen zijn gewijzigd."; + /* No comment provided by engineer. */ "Shape profile images" = "Vorm profiel afbeeldingen"; -/* chat item action */ +/* 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 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"; @@ -3539,6 +4751,12 @@ /* 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"; @@ -3548,12 +4766,24 @@ /* 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."; @@ -3570,7 +4800,7 @@ "SimpleX links" = "SimpleX links"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "SimpleX-links zijn in deze groep verboden."; +"SimpleX links are prohibited." = "SimpleX-links zijn niet toegestaan."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "SimpleX-links zijn niet toegestaan"; @@ -3590,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"; @@ -3603,14 +4839,36 @@ "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. */ "Square, circle, or anything in between." = "Vierkant, cirkel of iets daartussenin."; @@ -3626,18 +4884,21 @@ /* 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" = "Stop chat"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Stop de chat om database acties mogelijk te maken"; - /* 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."; @@ -3653,10 +4914,10 @@ /* 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 */ @@ -3666,20 +4927,44 @@ "Stopping chat" = "Chat stoppen"; /* No comment provided by engineer. */ -"strike" = "staking"; +"Storage" = "Opslag"; /* No comment provided by engineer. */ -"Submit" = "Indienen"; +"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"; @@ -3687,29 +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 paste link" = "Tik om de link te plakken"; +"Tap to join incognito" = "Tik hier om incognito lid te worden"; /* No comment provided by engineer. */ -"Tap to scan" = "Tik om te scannen"; +"Tap to paste link" = "Tik hier om de link te plakken"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Tik om een nieuw gesprek te starten"; +"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"; @@ -3719,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. */ @@ -3741,10 +5038,13 @@ "Thanks to the users – contribute via Weblate!" = "Dank aan de gebruikers – draag bij via Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Het eerste platform zonder gebruikers-ID's, privé door ontwerp."; +"The 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 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."; +"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."; @@ -3752,6 +5052,9 @@ /* 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!"; @@ -3764,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."; @@ -3777,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! ✅"; @@ -3792,13 +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. */ +"The servers for new files of your current chat profile **%@**." = "De servers voor nieuwe bestanden van uw huidige chatprofiel **%@**."; /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "De tekst die u hebt geplakt is geen SimpleX link."; /* No comment provided by engineer. */ -"Theme" = "Thema"; +"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 **%@**."; @@ -3812,8 +5136,11 @@ /* 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."; @@ -3825,7 +5152,7 @@ "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."; @@ -3843,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:"; @@ -3858,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."; @@ -3866,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: %@)."; @@ -3920,20 +5292,20 @@ /* rcv group event chat item */ "unblocked %@" = "gedeblokkeerd %@"; -/* item status description */ -"Unexpected error: %@" = "Onverwachte fout: %@"; +/* 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"; @@ -3953,6 +5325,12 @@ /* 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"; @@ -3974,10 +5352,13 @@ /* 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. */ @@ -3986,9 +5367,6 @@ /* No comment provided by engineer. */ "Update" = "Update"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = ".onion hosts-instelling updaten?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Database wachtwoord bijwerken"; @@ -3996,7 +5374,10 @@ "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"; @@ -4008,10 +5389,10 @@ "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"; @@ -4019,18 +5400,33 @@ /* 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"; @@ -4046,23 +5442,44 @@ /* 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. */ +"Use SOCKS proxy" = "Gebruik SOCKS proxy"; + +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "Gebruik TCP-poort %@ als er geen poort is opgegeven."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Gebruik de app tijdens het gesprek."; /* No comment provided by engineer. */ -"User profile" = "Gebruikers profiel"; +"Use the app with one hand." = "Gebruik de app met één hand."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Het gebruik van .onion-hosts vereist een compatibele VPN-provider."; +"Use web port" = "Gebruik een webpoort"; /* No comment provided by engineer. */ -"Using SimpleX Chat servers." = "SimpleX Chat servers gebruiken."; +"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%@"; @@ -4109,6 +5526,9 @@ /* 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"; @@ -4124,9 +5544,15 @@ /* 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"; @@ -4137,16 +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 not allowed" = "Spraakberichten niet toegestaan"; /* No comment provided by engineer. */ -"Voice messages prohibited!" = "Spraak berichten verboden!"; +"Voice messages prohibited!" = "Spraak berichten niet toegestaan!"; /* No comment provided by engineer. */ "waiting for answer…" = "wachten op antwoord…"; @@ -4166,6 +5592,12 @@ /* 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!"; @@ -4185,10 +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" = "Welkomstbericht is te lang"; +"Welcome message is too long" = "Welkom bericht is te lang"; /* No comment provided by engineer. */ "What's new" = "Wat is er nieuw"; @@ -4200,7 +5632,10 @@ "When connecting audio and video calls." = "Bij het verbinden van audio- en video-oproepen."; /* 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 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."; @@ -4218,19 +5653,31 @@ "With encrypted files and media." = "‐Met versleutelde bestanden en media."; /* No comment provided by engineer. */ -"With optional welcome message." = "Met optioneel welkomst bericht."; +"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"; @@ -4238,9 +5685,6 @@ /* No comment provided by engineer. */ "you" = "jij"; -/* No comment provided by engineer. */ -"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."; @@ -4251,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 %@."; @@ -4287,7 +5734,10 @@ "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"; @@ -4295,6 +5745,12 @@ /* 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 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"; @@ -4314,7 +5770,13 @@ "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."; @@ -4326,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."; @@ -4337,7 +5799,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "U kunt markdown gebruiken voor opmaak in berichten:"; -/* No comment provided by engineer. */ +/* 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. */ @@ -4356,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!"; @@ -4367,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."; @@ -4383,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."; @@ -4406,6 +5874,9 @@ /* 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"; @@ -4430,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."; @@ -4445,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"; @@ -4457,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 (%@)."; @@ -4472,6 +5946,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Uw contacten blijven verbonden."; +/* No comment provided by engineer. */ +"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."; @@ -4494,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."; @@ -4503,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"; @@ -4514,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 15262085eb..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ć)"; @@ -31,30 +16,15 @@ /* No comment provided by engineer. */ "- 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 pokolorowany!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(nowy)"; /* 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,7 @@ "[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 contact**: to create a new invitation link, or connect via a link you received." = "**Dodaj kontakt**: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku."; - -/* 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 group**: to create a new group." = "**Utwórz grupę**: aby utworzyć nową grupę."; @@ -80,10 +47,10 @@ "**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 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."; +"**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, wiadomości sprawdzane są co jakiś czas w tle (zależne od tego 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. */ "**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ń."; @@ -92,7 +59,10 @@ "**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**: 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."; +"**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 / 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ą zapisania kodu dostępu w Keychain."; @@ -155,7 +125,10 @@ "%@ 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"; @@ -163,6 +136,9 @@ /* 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"; @@ -175,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"; @@ -187,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)"; @@ -229,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"; @@ -277,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 dzień"; /* time interval */ @@ -286,12 +278,23 @@ /* 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" = "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"; @@ -326,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"; @@ -338,54 +338,104 @@ "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 contact" = "Dodaj kontakt"; +"Add friends" = "Dodaj znajomych"; /* No comment provided by engineer. */ -"Add preset servers" = "Dodaj gotowe serwery"; +"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"; @@ -401,27 +451,42 @@ /* 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ąć!"; @@ -431,6 +496,12 @@ /* 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."; @@ -446,9 +517,15 @@ /* 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 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)"; @@ -464,9 +541,15 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Zezwól na wysyłanie znikających wiadomości."; +/* No comment provided by engineer. */ +"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."; @@ -509,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"; @@ -518,9 +604,15 @@ /* 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: %@"; @@ -530,6 +622,9 @@ /* 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"; @@ -539,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"; @@ -551,15 +649,45 @@ /* 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"; @@ -599,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"; @@ -617,12 +751,33 @@ /* 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"; @@ -647,12 +802,19 @@ /* rcv group event chat item */ "blocked %@" = "zablokowany %@"; -/* marked deleted chat item preview text */ +/* 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"; @@ -674,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!"; @@ -692,9 +866,18 @@ /* No comment provided by engineer. */ "Calls" = "Połączenia"; +/* No comment provided by engineer. */ +"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!"; @@ -702,6 +885,10 @@ "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. */ @@ -714,14 +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?"; @@ -747,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 */ @@ -766,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"; @@ -778,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"; @@ -789,16 +997,40 @@ /* 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. */ @@ -814,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. */ @@ -829,10 +1070,13 @@ "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"; @@ -843,15 +1087,36 @@ /* 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"; @@ -885,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ą?"; @@ -909,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)"; @@ -942,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"; @@ -960,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!"; @@ -969,9 +1255,15 @@ /* 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$@"; @@ -981,6 +1273,9 @@ /* 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"; @@ -994,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"; @@ -1002,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"; @@ -1011,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 %@?"; @@ -1026,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"; @@ -1059,15 +1363,15 @@ /* No comment provided by engineer. */ "Create your profile" = "Utwórz swój profil"; +/* No comment provided by engineer. */ +"Created" = "Utworzono"; + /* No comment provided by engineer. */ "Created at" = "Utworzony o"; /* copied message info */ "Created at: %@" = "Utworzony o: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Utworzony w dniu %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Tworzenie linku archiwum"; @@ -1083,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 %@."; @@ -1092,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"; @@ -1155,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. */ @@ -1170,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?"; @@ -1191,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"; @@ -1210,10 +1528,7 @@ "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"; @@ -1257,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. */ @@ -1269,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?"; @@ -1281,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"; @@ -1299,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"; @@ -1320,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"; @@ -1348,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)"; @@ -1362,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ść"; @@ -1372,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"; @@ -1398,6 +1743,15 @@ /* 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."; @@ -1413,15 +1767,28 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Obniż wersję i otwórz czat"; -/* chat item action */ +/* 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"; @@ -1434,6 +1801,9 @@ /* integrity error chat item */ "duplicate message" = "zduplikowana wiadomość"; +/* No comment provided by engineer. */ +"duplicates" = "duplikaty"; + /* No comment provided by engineer. */ "Duration" = "Czas trwania"; @@ -1452,7 +1822,7 @@ /* 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. */ @@ -1491,6 +1861,9 @@ /* enabled status */ "enabled" = "włączone"; +/* No comment provided by engineer. */ +"Enabled" = "Włączony"; + /* No comment provided by engineer. */ "Enabled for" = "Włączony dla"; @@ -1617,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"; @@ -1662,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"; @@ -1692,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"; @@ -1699,19 +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"; -/* No comment provided by engineer. */ +/* 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"; @@ -1753,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. */ @@ -1779,7 +2167,9 @@ /* No comment provided by engineer. */ "Error: " = "Błąd: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Błąd: %@"; /* No comment provided by engineer. */ @@ -1789,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"; @@ -1800,12 +2190,18 @@ /* 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."; @@ -1824,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."; @@ -1839,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"; @@ -1846,7 +2263,7 @@ "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"; @@ -1893,9 +2310,18 @@ /* 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"; @@ -1905,6 +2331,24 @@ /* 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"; @@ -1917,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."; @@ -1932,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"; @@ -1968,27 +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. (24 hours)" = "Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny)"; - -/* 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 SimpleX links." = "Członkowie grupy mogą wysyłać linki SimpleX."; - -/* 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:"; @@ -2049,9 +2475,6 @@ /* 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"; @@ -2095,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"; @@ -2109,6 +2532,9 @@ /* 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"; @@ -2130,6 +2556,9 @@ /* 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"; @@ -2184,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"; @@ -2223,7 +2655,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Nieprawidłowa odpowiedź"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Nieprawidłowy adres serwera!"; /* item status text */ @@ -2235,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"; @@ -2262,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"; @@ -2269,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."; @@ -2280,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 (%@)."; @@ -2292,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. */ @@ -2319,13 +2760,16 @@ /* No comment provided by engineer. */ "Joining group" = "Dołączanie do grupy"; -/* No comment provided by engineer. */ +/* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Zachować nieużyte zaproszenie?"; /* No comment provided by engineer. */ @@ -2343,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. */ @@ -2382,9 +2826,6 @@ /* No comment provided by engineer. */ "Live messages" = "Wiadomości na żywo"; -/* No comment provided by engineer. */ -"Local" = "Lokalnie"; - /* No comment provided by engineer. */ "Local name" = "Nazwa lokalna"; @@ -2397,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"; @@ -2433,6 +2865,12 @@ /* 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"; @@ -2445,6 +2883,9 @@ /* 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."; @@ -2454,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"; @@ -2470,14 +2950,29 @@ "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"; @@ -2493,6 +2988,15 @@ /* 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."; @@ -2533,7 +3037,7 @@ "Migration is completed" = "Migracja została zakończona"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migracje: %@"; +"Migrations:" = "Migracje:"; /* time unit */ "minutes" = "minuty"; @@ -2568,19 +3072,16 @@ /* 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. */ @@ -2589,6 +3090,9 @@ /* 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"; @@ -2598,21 +3102,21 @@ /* 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!"; @@ -2622,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"; @@ -2637,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"; @@ -2658,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"; @@ -2670,21 +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"; @@ -2698,11 +3235,11 @@ "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"; /* feature offered item */ @@ -2711,7 +3248,7 @@ /* feature offered item */ "offered %@: %@" = "zaoferował %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2720,9 +3257,6 @@ /* No comment provided by engineer. */ "Old database" = "Stara baza danych"; -/* No comment provided by engineer. */ -"Old database archive" = "Stare archiwum bazy danych"; - /* group pref value */ "on" = "włączone"; @@ -2730,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."; @@ -2780,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. */ @@ -2798,12 +3335,6 @@ /* 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. */ -"Open-source protocol and code – anybody can run the servers." = "Otwarto źródłowy protokół i kod - każdy może uruchomić serwery."; - /* No comment provided by engineer. */ "Opening app…" = "Otwieranie aplikacji…"; @@ -2819,9 +3350,18 @@ /* 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"; @@ -2843,6 +3383,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "Pin ustawiony!"; +/* No comment provided by engineer. */ +"Password" = "Hasło"; + /* No comment provided by engineer. */ "Password to show" = "Hasło do wyświetlenia"; @@ -2865,10 +3408,10 @@ "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"; @@ -2882,9 +3425,18 @@ /* 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."; @@ -2927,21 +3479,24 @@ /* 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"; @@ -2951,9 +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"; @@ -2963,16 +3530,13 @@ /* No comment provided by engineer. */ "Profile images" = "Zdjęcia profilowe"; -/* No comment provided by engineer. */ -"Profile name" = "Nazwa profilu"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nazwa profilu:"; - /* 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. */ @@ -3005,15 +3569,30 @@ /* 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"; @@ -3029,20 +3608,23 @@ /* 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/chat-profiles.html#incognito-mode)." = "Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](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)."; @@ -3051,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ź…"; @@ -3075,10 +3657,16 @@ "Received message" = "Otrzymano wiadomość"; /* 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."; +"Received messages" = "Otrzymane wiadomości"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Konkurencyjne odbieranie"; +"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."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Odbieranie pliku zostanie przerwane."; @@ -3095,9 +3683,24 @@ /* 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?"; @@ -3110,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. */ @@ -3131,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"; @@ -3188,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"; @@ -3218,9 +3843,6 @@ /* chat item action */ "Reveal" = "Ujawnij"; -/* No comment provided by engineer. */ -"Revert" = "Przywrócić"; - /* No comment provided by engineer. */ "Revoke" = "Odwołaj"; @@ -3236,30 +3858,31 @@ /* No comment provided by engineer. */ "Run chat" = "Uruchom czat"; +/* No comment provided by engineer. */ +"Safely receive files" = "Bezpiecznie otrzymuj pliki"; + /* No comment provided by engineer. */ "Safer groups" = "Bezpieczniejsze grupy"; -/* chat item action */ +/* 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"; @@ -3269,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. */ @@ -3278,15 +3901,15 @@ /* 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"; @@ -3305,6 +3928,15 @@ /* 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"; @@ -3320,6 +3952,9 @@ /* 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"; @@ -3332,6 +3967,9 @@ /* network option */ "sec" = "sek"; +/* No comment provided by engineer. */ +"Secondary" = "Drugorzędny"; + /* time unit */ "seconds" = "sekundy"; @@ -3341,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"; @@ -3350,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"; @@ -3377,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"; @@ -3393,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"; @@ -3410,7 +4066,7 @@ /* 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."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Nadawca anulował transfer pliku."; /* No comment provided by engineer. */ @@ -3446,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"; @@ -3464,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"; @@ -3476,6 +4177,9 @@ /* 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"; @@ -3506,10 +4210,14 @@ /* No comment provided by engineer. */ "Settings" = "Ustawienia"; +/* alert message */ +"Settings were changed." = "Ustawienia zostały zmienione."; + /* No comment provided by engineer. */ "Shape profile images" = "Kształtuj obrazy profilowe"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "Udostępnij"; /* No comment provided by engineer. */ @@ -3518,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 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"; @@ -3539,6 +4259,12 @@ /* 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"; @@ -3548,6 +4274,9 @@ /* No comment provided by engineer. */ "Show:" = "Pokaż:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Adres SimpleX"; @@ -3570,7 +4299,7 @@ "SimpleX links" = "Linki SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "Linki SimpleX są zablokowane na tej grupie."; +"SimpleX links are prohibited." = "Linki SimpleX są zablokowane na tej grupie."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "Linki SimpleX są niedozwolone"; @@ -3593,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ń"; @@ -3603,11 +4335,26 @@ "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ś"; @@ -3626,18 +4373,21 @@ /* 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" = "Zatrzymaj czat"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Zatrzymaj czat, aby umożliwić działania na bazie danych"; - /* 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."; @@ -3653,10 +4403,10 @@ /* 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 */ @@ -3668,9 +4418,21 @@ /* 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"; @@ -3680,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"; @@ -3705,7 +4470,7 @@ "Tap to scan" = "Dotknij, aby zeskanować"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Dotknij, aby rozpocząć nowy czat"; +"TCP connection" = "Połączenie TCP"; /* No comment provided by engineer. */ "TCP connection timeout" = "Limit czasu połączenia TCP"; @@ -3719,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 %@."; @@ -3728,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. */ @@ -3741,10 +4509,10 @@ "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."; +"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 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ć."; +"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."; @@ -3764,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."; @@ -3777,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! ✅"; @@ -3798,7 +4572,10 @@ "The text you pasted is not a SimpleX link." = "Tekst, który wkleiłeś nie jest linkiem SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Motyw"; +"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. */ +"Themes" = "Motywy"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Te ustawienia dotyczą Twojego bieżącego profilu **%@**."; @@ -3842,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:"; @@ -3857,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."; @@ -3878,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: %@)."; @@ -3920,13 +4724,10 @@ /* rcv group event chat item */ "unblocked %@" = "odblokowano %@"; -/* item status description */ -"Unexpected error: %@" = "Nieoczekiwany błąd: %@"; - /* 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. */ @@ -3953,6 +4754,12 @@ /* 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"; @@ -3974,10 +4781,13 @@ /* 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. */ @@ -3986,9 +4796,6 @@ /* No comment provided by engineer. */ "Update" = "Aktualizuj"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Zaktualizować ustawienie hostów .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Aktualizuj hasło do bazy danych"; @@ -3996,7 +4803,7 @@ "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"; @@ -4008,10 +4815,10 @@ "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"; @@ -4019,6 +4826,12 @@ /* 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"; @@ -4046,20 +4859,32 @@ /* 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"; /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Użyć serwerów SimpleX Chat?"; +/* No comment provided by engineer. */ +"Use SOCKS proxy" = "Użyj proxy SOCKS"; + /* No comment provided by engineer. */ "Use the app while in the call." = "Używaj aplikacji podczas połączenia."; /* No comment provided by engineer. */ -"User profile" = "Profil użytkownika"; +"Use the app with one hand." = "Korzystaj z aplikacji jedną ręką."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Używanie hostów .onion wymaga kompatybilnego dostawcy VPN."; +"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."; @@ -4109,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"; @@ -4140,7 +4968,7 @@ "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"; @@ -4166,6 +4994,12 @@ /* 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ć!"; @@ -4200,7 +5034,7 @@ "When connecting audio and video calls." = "Podczas łączenia połączeń audio i wideo."; /* 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 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."; @@ -4223,14 +5057,26 @@ /* 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"; @@ -4238,9 +5084,6 @@ /* No comment provided by engineer. */ "you" = "Ty"; -/* No comment provided by engineer. */ -"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."; @@ -4286,6 +5129,9 @@ /* 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"; @@ -4295,6 +5141,9 @@ /* 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 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"; @@ -4314,7 +5163,10 @@ "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."; @@ -4326,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."; @@ -4337,7 +5189,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Możesz używać markdown do formatowania wiadomości:"; -/* No comment provided by engineer. */ +/* 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. */ @@ -4356,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!"; @@ -4367,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."; @@ -4385,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ć."; @@ -4445,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"; @@ -4457,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 (%@)."; @@ -4472,6 +5330,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Twoje kontakty pozostaną połączone."; +/* No comment provided by engineer. */ +"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ą."; @@ -4494,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."; @@ -4503,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"; @@ -4514,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 2c703f0095..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,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Поставить звездочку в GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Добавить контакт**: создать новую ссылку-приглашение или подключиться через полученную ссылку."; - -/* 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 group**: to create a new group." = "**Создать группу**: создать новую группу."; @@ -80,10 +47,10 @@ "**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. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Обратите внимание**: использование одной и той же базы данных на двух устройствах нарушит расшифровку сообщений от ваших контактов, как свойство защиты соединений."; @@ -92,7 +59,10 @@ "**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 / Paste link**: to connect via a link you received." = "**Сканировать / Вставить ссылку**: чтобы соединиться через полученную ссылку."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain."; @@ -154,6 +124,9 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ подтверждён"; +/* No comment provided by engineer. */ +"%@ server" = "%@ сервер"; + /* No comment provided by engineer. */ "%@ servers" = "%@ серверы"; @@ -163,11 +136,14 @@ /* 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 */ "%@:" = "%@:"; @@ -175,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 мин"; @@ -187,6 +178,9 @@ /* time interval */ "%d sec" = "%d сек"; +/* delete after time */ +"%d seconds(s)" = "%d секунд"; + /* integrity error chat item */ "%d skipped message(s)" = "%d пропущенных сообщение(й)"; @@ -209,7 +203,7 @@ "%lld group events" = "%lld событий"; /* No comment provided by engineer. */ -"%lld members" = "Членов группы: %lld"; +"%lld members" = "%lld членов"; /* No comment provided by engineer. */ "%lld messages blocked" = "%lld сообщений заблокировано"; @@ -229,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 секунд"; @@ -277,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0с"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 день"; /* time interval */ @@ -286,12 +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 минут"; @@ -314,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" = "Прекратить"; @@ -326,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"; @@ -338,54 +338,107 @@ "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 contact" = "Добавить контакт"; +"Add friends" = "Добавить друзей"; /* No comment provided by engineer. */ -"Add preset servers" = "Добавить серверы по умолчанию"; +"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" = "админ"; @@ -401,26 +454,38 @@ /* 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." = "Все данные хранятся только на вашем устройстве."; -/* feature role */ -"all members" = "все члены"; +/* 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!" = "Все сообщения будут удалены - это нельзя отменить!"; @@ -431,6 +496,12 @@ /* 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." = "Все контакты, которые соединились через этот адрес, сохранятся."; @@ -446,9 +517,15 @@ /* 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 downgrade" = "Разрешить прямую доставку"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа)"; @@ -459,14 +536,20 @@ "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 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." = "Разрешить посылать файлы и медиа."; @@ -509,6 +592,9 @@ /* pref value */ "always" = "всегда"; +/* No comment provided by engineer. */ +"Always use private routing." = "Всегда использовать конфиденциальную доставку."; + /* No comment provided by engineer. */ "Always use relay" = "Всегда соединяться через relay"; @@ -518,9 +604,15 @@ /* 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: %@" = "Сборка приложения: %@"; @@ -530,6 +622,9 @@ /* 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" = "Иконка"; @@ -539,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" = "Версия приложения"; @@ -551,15 +649,48 @@ /* 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" = "Аудио- и видеозвонки"; @@ -599,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" = "Неверный адрес компьютера"; @@ -617,12 +754,39 @@ /* 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" = "Заблокировать"; @@ -630,13 +794,13 @@ "Block for all" = "Заблокировать для всех"; /* No comment provided by engineer. */ -"Block group members" = "Блокируйте членов группы"; +"Block group members" = "Заблокировать членов группы"; /* No comment provided by engineer. */ "Block member" = "Заблокировать члена группы"; /* No comment provided by engineer. */ -"Block member for all?" = "Заблокировать члена для всех?"; +"Block member for all?" = "Заблокировать для всех?"; /* No comment provided by engineer. */ "Block member?" = "Заблокировать члена группы?"; @@ -647,12 +811,19 @@ /* rcv group event chat item */ "blocked %@" = "%@ заблокирован"; -/* marked deleted chat item preview text */ +/* 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" = "жирный"; @@ -674,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!" = "Звонок уже завершен!"; @@ -692,9 +878,18 @@ /* No comment provided by engineer. */ "Calls" = "Звонки"; +/* No comment provided by engineer. */ +"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!" = "Нельзя пригласить контакт!"; @@ -702,6 +897,10 @@ "Can't invite contacts!" = "Нельзя пригласить контакты!"; /* No comment provided by engineer. */ +"Can't message member" = "Не удаётся отправить сообщение члену группы"; + +/* alert action +alert button */ "Cancel" = "Отменить"; /* No comment provided by engineer. */ @@ -714,14 +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?" = "Поменять пароль базы данных?"; @@ -747,7 +958,7 @@ "Change self-destruct mode" = "Изменить режим самоуничтожения"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Изменить код самоуничтожения"; /* chat item text */ @@ -766,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" = "Консоль"; @@ -777,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" = "Архив чата импортирован"; @@ -789,16 +1012,40 @@ /* 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. */ @@ -814,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. */ @@ -822,6 +1078,12 @@ /* 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?" = "Очистить личные заметки?"; @@ -829,10 +1091,16 @@ "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" = "цвет"; + +/* report reason */ +"Community guidelines violation" = "Нарушение правил группы"; /* server test step */ "Compare file" = "Сравнение файла"; @@ -843,15 +1111,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. */ +"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" = "Подтвердите настройки сети"; @@ -870,6 +1171,9 @@ /* No comment provided by engineer. */ "Confirm upload" = "Подтвердить загрузку"; +/* token status text */ +"Confirmed" = "Подтвержденный"; + /* server test step */ "Connect" = "Соединиться"; @@ -885,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?" = "Соединиться с самим собой?"; @@ -909,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)" = "соединяется (приглашение принято)"; @@ -942,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" = "Ошибка соединения"; @@ -960,18 +1285,39 @@ /* 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$@"; @@ -981,6 +1327,9 @@ /* 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 шифрование"; @@ -994,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" = "Имена контактов"; @@ -1002,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 %@?" = "Исправить имя на %@?"; @@ -1024,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" = "Создание файла"; @@ -1041,6 +1405,9 @@ /* 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/). 💻"; @@ -1059,15 +1426,15 @@ /* No comment provided by engineer. */ "Create your profile" = "Создать профиль"; +/* No comment provided by engineer. */ +"Created" = "Создано"; + /* No comment provided by engineer. */ "Created at" = "Создано"; /* copied message info */ "Created at: %@" = "Создано: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Дата создания %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Создание ссылки на архив"; @@ -1077,12 +1444,18 @@ /* 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 %@." = "Максимальный размер файла - %@."; @@ -1092,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" = "Откат базы данных"; @@ -1155,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. */ @@ -1170,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 сообщений?"; @@ -1192,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" = "Удалить профиль чата"; @@ -1203,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" = "Удалить соединение"; @@ -1210,10 +1606,7 @@ "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" = "Удалить данные чата"; @@ -1251,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. */ @@ -1270,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?" = "Удалить ожидаемое соединение?"; @@ -1281,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" = "Удалено"; @@ -1299,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" = "Доставка"; @@ -1320,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" = "Инструменты разработчика"; @@ -1348,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" = "Выключить для всех"; @@ -1362,6 +1800,9 @@ /* No comment provided by engineer. */ "disabled" = "выключено"; +/* No comment provided by engineer. */ +"Disabled" = "Выключено"; + /* No comment provided by engineer. */ "Disappearing message" = "Исчезающее сообщение"; @@ -1372,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" = "Исчезает"; @@ -1396,32 +1837,60 @@ "Do it later" = "Отложить"; /* No comment provided by engineer. */ -"Do not send history to new members." = "Не отправлять историю новым членам."; +"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" = "Откатить версию и открыть чат"; -/* chat item action */ +/* 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" = "Загрузка архива"; @@ -1434,12 +1903,18 @@ /* 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" = "Редактировать"; @@ -1452,7 +1927,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. */ @@ -1491,6 +1966,9 @@ /* enabled status */ "enabled" = "включено"; +/* No comment provided by engineer. */ +"Enabled" = "Включено"; + /* No comment provided by engineer. */ "Enabled for" = "Включено для"; @@ -1566,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" = "завершён"; @@ -1614,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" = "Ошибка при создании адреса"; @@ -1641,8 +2134,8 @@ /* No comment provided by engineer. */ "Error creating group link" = "Ошибка при создании ссылки группы"; -/* No comment provided by engineer. */ -"Error creating member contact" = "Ошибка создания контакта с членом группы"; +/* alert title */ +"Error creating list" = "Ошибка создания списка"; /* No comment provided by engineer. */ "Error creating message" = "Ошибка создания сообщения"; @@ -1650,6 +2143,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Ошибка создания профиля!"; +/* No comment provided by engineer. */ +"Error creating report" = "Ошибка создания сообщения о нарушении"; + /* No comment provided by engineer. */ "Error decrypting file" = "Ошибка расшифровки файла"; @@ -1662,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" = "Ошибка при удалении данных чата"; @@ -1692,26 +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" = "Ошибка при вступлении в группу"; +/* alert title */ +"Error loading servers" = "Ошибка загрузки серверов"; + /* No comment provided by engineer. */ -"Error loading %@ servers" = "Ошибка загрузки %@ серверов"; +"Error migrating settings" = "Ошибка миграции настроек"; /* No comment provided by engineer. */ "Error opening chat" = "Ошибка доступа к чату"; -/* No comment provided by engineer. */ +/* 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" = "Ошибка при сохранении профиля группы"; @@ -1725,6 +2236,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Ошибка сохранения пароля в Keychain"; +/* alert title */ +"Error saving servers" = "Ошибка сохранения серверов"; + /* when migrating */ "Error saving settings" = "Ошибка сохранения настроек"; @@ -1737,9 +2251,6 @@ /* No comment provided by engineer. */ "Error sending email" = "Ошибка отправки email"; -/* No comment provided by engineer. */ -"Error sending member contact invitation" = "Ошибка отправки приглашения члену группы"; - /* No comment provided by engineer. */ "Error sending message" = "Ошибка при отправке сообщения"; @@ -1753,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" = "Ошибка при сохранении настроек сети"; @@ -1779,7 +2299,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. */ @@ -1789,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" = "Выйти без сохранения"; @@ -1800,12 +2325,21 @@ /* 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." = "Архив чата экспортирован."; @@ -1821,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." = "Файл будет удалён с серверов."; @@ -1839,6 +2403,9 @@ /* No comment provided by engineer. */ "File: %@" = "Файл: %@"; +/* No comment provided by engineer. */ +"Files" = "Файлы"; + /* No comment provided by engineer. */ "Files & media" = "Файлы и медиа"; @@ -1846,7 +2413,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 not allowed" = "Файлы и медиа не разрешены"; @@ -1885,17 +2452,44 @@ "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" = "переслано"; @@ -1905,6 +2499,24 @@ /* 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" = "Компьютер найден"; @@ -1917,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" = "Группа"; @@ -1968,27 +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. (24 hours)" = "Члены группы могут необратимо удалять отправленные сообщения. (24 часа)"; - -/* 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 SimpleX links." = "Члены группы могут отправлять ссылки SimpleX."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; - /* notification */ "Group message:" = "Групповое сообщение:"; @@ -2001,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" = "Скрытое"; @@ -2043,13 +2637,16 @@ /* 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. */ @@ -2059,7 +2656,7 @@ "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" = "Как использовать серверы"; @@ -2095,7 +2692,7 @@ "Immediately" = "Сразу"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Защищен от спама"; +"Immune to spam" = "Защищен от спама"; /* No comment provided by engineer. */ "Import" = "Импортировать"; @@ -2109,9 +2706,15 @@ /* 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" = "Улучшенная доставка сообщений"; @@ -2130,6 +2733,15 @@ /* 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" = "Инкогнито"; @@ -2184,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" = "ошибка чата"; @@ -2223,7 +2853,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Ошибка ответа"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Ошибка в адресе сервера!"; /* item status text */ @@ -2235,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" = "Пригласить в группу"; @@ -2262,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" = "Окончательное удаление сообщений"; @@ -2269,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." = "Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя."; @@ -2280,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 (%@)." = "Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@)."; @@ -2292,7 +2931,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японский интерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Вступить"; /* No comment provided by engineer. */ @@ -2319,13 +2958,16 @@ /* No comment provided by engineer. */ "Joining group" = "Вступление в группу"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Оставить"; +/* No comment provided by engineer. */ +"Keep conversation" = "Оставить разговор"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Оставьте приложение открытым, чтобы использовать его с компьютера"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Оставить неиспользованное приглашение?"; /* No comment provided by engineer. */ @@ -2343,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" = "Выйти из группы"; @@ -2373,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"; @@ -2382,9 +3039,6 @@ /* No comment provided by engineer. */ "Live messages" = "\"Живые\" сообщения"; -/* No comment provided by engineer. */ -"Local" = "Локальные"; - /* No comment provided by engineer. */ "Local name" = "Локальное имя"; @@ -2397,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" = "Пометить как удаленное для всех"; @@ -2433,26 +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" = "Серверы файлов и медиа"; -/* profile update event chat item */ -"member %@ changed to %@" = "член %1$@ изменился на %2$@"; +/* 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" = "Ошибка доставки сообщения"; @@ -2460,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" = "Реакции на сообщения"; @@ -2470,14 +3124,29 @@ "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" = "Текст сообщения"; @@ -2493,6 +3162,18 @@ /* 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), правдоподобным отрицанием и восстановлением от взлома."; @@ -2533,7 +3214,7 @@ "Migration is completed" = "Перемещение данных завершено"; /* No comment provided by engineer. */ -"Migrations: %@" = "Миграции: %@"; +"Migrations:" = "Миграции:"; /* time unit */ "minutes" = "минут"; @@ -2556,31 +3237,40 @@ /* 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. */ @@ -2589,41 +3279,56 @@ /* 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" = "новое сообщение"; @@ -2637,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" = "нет"; @@ -2646,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" = "Контакты не выбраны"; @@ -2658,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 шифрования"; @@ -2670,39 +3396,96 @@ /* 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" = "Выключено"; /* feature offered item */ @@ -2711,7 +3494,7 @@ /* feature offered item */ "offered %@: %@" = "предложил(a) %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ок"; /* No comment provided by engineer. */ @@ -2720,9 +3503,6 @@ /* No comment provided by engineer. */ "Old database" = "Предыдущая версия данных чата"; -/* No comment provided by engineer. */ -"Old database archive" = "Старый архив чата"; - /* group pref value */ "on" = "да"; @@ -2730,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." = "Только владельцы группы могут изменять предпочтения группы."; @@ -2750,6 +3536,12 @@ /* 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." = "Только Вы можете добавлять реакции на сообщения."; @@ -2780,15 +3572,21 @@ /* 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" = "Открыть группу"; @@ -2798,15 +3596,18 @@ /* 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 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" = "Или вставьте ссылку архива"; @@ -2819,9 +3620,24 @@ /* 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" = "владелец"; @@ -2844,10 +3660,10 @@ "Passcode set!" = "Код доступа установлен!"; /* No comment provided by engineer. */ -"Password to show" = "Пароль чтобы раскрыть"; +"Password" = "Пароль"; -/* past/unknown group member */ -"Past member %@" = "Бывший член %@"; +/* No comment provided by engineer. */ +"Password to show" = "Пароль чтобы раскрыть"; /* No comment provided by engineer. */ "Paste desktop address" = "Вставить адрес компьютера"; @@ -2865,10 +3681,16 @@ "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" = "Ошибка расшифровки"; @@ -2882,9 +3704,18 @@ /* 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." = "Пожалуйста, проверьте, что Вы использовали правильную ссылку или попросите, чтобы Ваш контакт отправил Вам другую ссылку."; @@ -2924,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" = "Возможно, хэш сертификата в адресе сервера неверный"; @@ -2934,26 +3777,53 @@ "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" = "Профиль и соединения на сервере"; @@ -2963,16 +3833,13 @@ /* No comment provided by engineer. */ "Profile images" = "Картинки профилей"; -/* No comment provided by engineer. */ -"Profile name" = "Имя профиля"; - -/* No comment provided by engineer. */ -"Profile name:" = "Имя профиля:"; - /* 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. */ @@ -2988,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." = "Запретить посылать исчезающие сообщения."; @@ -3005,15 +3872,30 @@ /* 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" = "Доставка уведомлений"; @@ -3029,20 +3911,23 @@ /* 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/chat-profiles.html#incognito-mode)." = "Дополнительная информация в [Руководстве пользователя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"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)."; @@ -3051,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…" = "получен ответ…"; @@ -3075,10 +3960,16 @@ "Received message" = "Полученное сообщение"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн."; +"Received messages" = "Полученные сообщения"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Одновременный приём"; +"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." = "Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Приём файла будет прекращён."; @@ -3095,9 +3986,24 @@ /* 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?" = "Переподключить серверы?"; @@ -3110,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. */ @@ -3119,6 +4035,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Отклонить запрос"; +/* No comment provided by engineer. */ +"rejected" = "отклонён"; + /* call status */ "rejected call" = "отклонённый звонок"; @@ -3132,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?"; @@ -3182,18 +4101,66 @@ /* 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" = "Перезапустите приложение, чтобы создать новый профиль."; @@ -3219,7 +4186,7 @@ "Reveal" = "Показать"; /* No comment provided by engineer. */ -"Revert" = "Отменить изменения"; +"Review conditions" = "Посмотреть условия"; /* No comment provided by engineer. */ "Revoke" = "Отозвать"; @@ -3236,40 +4203,41 @@ /* No comment provided by engineer. */ "Run chat" = "Запустить chat"; +/* No comment provided by engineer. */ +"Safely receive files" = "Получайте файлы безопасно"; + /* No comment provided by engineer. */ "Safer groups" = "Более безопасные группы"; -/* 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. */ -"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. */ @@ -3278,15 +4246,15 @@ /* 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" = "сохранено"; @@ -3305,6 +4273,15 @@ /* 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" = "Сканировать код"; @@ -3320,6 +4297,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Сканировать QR код сервера"; +/* No comment provided by engineer. */ +"search" = "поиск"; + /* No comment provided by engineer. */ "Search" = "Поиск"; @@ -3332,6 +4312,9 @@ /* network option */ "sec" = "сек"; +/* No comment provided by engineer. */ +"Secondary" = "Вторичный"; + /* time unit */ "seconds" = "секунд"; @@ -3341,6 +4324,9 @@ /* server test step */ "Secure queue" = "Защита очереди"; +/* No comment provided by engineer. */ +"Secured" = "Защищено"; + /* No comment provided by engineer. */ "Security assessment" = "Аудит безопасности"; @@ -3350,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" = "Самоуничтожение"; @@ -3377,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" = "Отправьте вопросы и идеи"; @@ -3407,10 +4411,7 @@ /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур."; -/* No comment provided by engineer. */ -"Send up to 100 last messages to new members." = "Отправить до 100 последних сообщений новым членам."; - -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Отправитель отменил передачу файла."; /* No comment provided by engineer. */ @@ -3446,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" = "Сервер требует авторизации для создания очередей, проверьте пароль"; @@ -3464,24 +4507,48 @@ /* 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" = "установлен новый адрес контакта"; @@ -3497,39 +4564,61 @@ /* No comment provided by engineer. */ "Set passphrase to export" = "Установите пароль"; -/* No comment provided by engineer. */ -"Set the message shown to new members!" = "Установить сообщение для новых членов группы!"; - /* No comment provided by engineer. */ "Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN"; /* No comment provided by engineer. */ "Settings" = "Настройки"; +/* alert message */ +"Settings were changed." = "Настройки были изменены."; + /* No comment provided by engineer. */ "Shape profile images" = "Форма картинок профилей"; -/* chat item action */ +/* 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 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" = "Показать звонки в истории телефона"; @@ -3539,6 +4628,12 @@ /* 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" = "Показывать уведомления"; @@ -3548,12 +4643,24 @@ /* 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."; @@ -3570,7 +4677,7 @@ "SimpleX links" = "SimpleX ссылки"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "Ссылки SimpleX запрещены в этой группе."; +"SimpleX links are prohibited." = "Ссылки SimpleX запрещены в этой группе."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "Ссылки SimpleX не разрешены"; @@ -3590,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" = "Пропустить"; @@ -3603,14 +4716,36 @@ "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. */ "Square, circle, or anything in between." = "Квадрат, круг и все, что между ними."; @@ -3626,18 +4761,21 @@ /* 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" = "Остановить чат"; -/* 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." = "Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен."; @@ -3653,10 +4791,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 */ @@ -3665,27 +4803,54 @@ /* 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." = "Нажмите, чтобы сделать профиль активным."; @@ -3705,11 +4870,14 @@ "Tap to scan" = "Нажмите, чтобы сканировать"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Нажмите, чтобы начать чат"; +"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"; @@ -3719,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. */ @@ -3741,10 +4915,13 @@ "Thanks to the users – contribute via Weblate!" = "Благодаря пользователям – добавьте переводы через Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Первая в мире платформа без идентификаторов пользователей."; +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; /* No comment provided by engineer. */ -"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; +"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." = "Попытка поменять пароль базы данных не была завершена."; @@ -3752,6 +4929,9 @@ /* 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!" = "Подтвержденное соединение будет отменено!"; @@ -3764,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! ✅" = "Вторая галочка - знать, что доставлено! ✅"; @@ -3794,11 +4974,20 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Серверы для новых соединений Вашего текущего профиля чата **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "Серверы для новых файлов Вашего текущего профиля **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Вставленный текст не является SimpleX-ссылкой."; /* No comment provided by engineer. */ -"Theme" = "Тема"; +"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 **%@**." = "Установки для Вашего активного профиля **%@**."; @@ -3812,6 +5001,9 @@ /* 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." = "Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; @@ -3830,9 +5022,6 @@ /* 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." = "Эта группа больше не существует."; @@ -3842,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:" = "Чтобы задать вопросы и получать уведомления о новых версиях,"; @@ -3858,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."; @@ -3866,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: %@)." = "Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %@)."; @@ -3908,25 +5142,16 @@ /* 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 %@" = "%@ разблокирован"; -/* item status description */ -"Unexpected error: %@" = "Неожиданная ошибка: %@"; +/* 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. */ @@ -3953,6 +5178,12 @@ /* 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" = "неизвестный статус"; @@ -3974,21 +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. */ -"Up to 100 last messages are sent to new members." = "До 100 последних сообщений отправляются новым членам."; - /* No comment provided by engineer. */ "Update" = "Обновить"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Обновить настройки .onion хостов?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Поменять пароль"; @@ -3996,7 +5224,10 @@ "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" = "обновил(а) профиль группы"; @@ -4008,10 +5239,10 @@ "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" = "Ошибка загрузки"; @@ -4019,18 +5250,33 @@ /* 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" = "Использовать для новых соединений"; @@ -4046,20 +5292,41 @@ /* 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. */ +"Use SOCKS proxy" = "Использовать SOCKS прокси"; + +/* No comment provided by engineer. */ +"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. */ -"User profile" = "Профиль чата"; +"Use the app with one hand." = "Используйте приложение одной рукой."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Для использования .onion хостов требуется совместимый VPN провайдер."; +"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."; @@ -4109,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" = "Видеозвонок"; @@ -4124,9 +5394,15 @@ /* 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" = "Доступ к истории"; @@ -4140,7 +5416,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 not allowed" = "Голосовые сообщения не разрешены"; @@ -4166,6 +5442,12 @@ /* 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!" = "хочет соединиться с Вами!"; @@ -4200,7 +5482,10 @@ "When connecting audio and video calls." = "Во время соединения аудио и видео звонков."; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Когда Вы получите запрос на соединение, Вы можете принять или отклонить его."; +"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." = "Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом."; @@ -4223,14 +5508,26 @@ /* 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" = "да"; @@ -4238,9 +5535,6 @@ /* No comment provided by engineer. */ "you" = "Вы"; -/* No comment provided by engineer. */ -"You" = "Вы"; - /* No comment provided by engineer. */ "You **must not** use the same database on two devices." = "Вы **не должны** использовать одну и ту же базу данных на двух устройствах."; @@ -4256,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 %@." = "Вы уже соединяетесь с %@."; @@ -4286,6 +5583,9 @@ /* 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" = "только чтение сообщений"; @@ -4295,6 +5595,12 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Вы можете принимать звонки на экране блокировки, без аутентификации."; +/* No comment provided by engineer. */ +"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" = "Вы можете создать его позже"; @@ -4314,22 +5620,25 @@ "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 через Настройки."; @@ -4337,7 +5646,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Вы можете форматировать сообщения:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Вы можете увидеть ссылку-приглашение снова открыв соединение."; /* No comment provided by engineer. */ @@ -4356,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!" = "Вы уже запросили соединение через этот адрес!"; @@ -4367,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." = "Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата."; @@ -4379,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." = "Чтобы включить отправку голосовых сообщений, разрешите их Вашему контакту."; @@ -4406,6 +5718,9 @@ /* chat list item description */ "you shared one-time link incognito" = "Вы создали ссылку инкогнито"; +/* token info */ +"You should receive notifications." = "Вы должны получать уведомления."; + /* snd group event chat item */ "you unblocked %@" = "Вы разблокировали %@"; @@ -4425,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." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена."; @@ -4445,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" = "Ваши звонки"; @@ -4457,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 (%@)." = "Ваш контакт отправил файл, размер которого превышает максимальный размер (%@)."; @@ -4472,6 +5787,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Ваши контакты сохранятся."; +/* No comment provided by engineer. */ +"Your credentials may be sent unencrypted." = "Ваши учетные данные могут быть отправлены в незашифрованном виде."; + /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Текущие данные Вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными."; @@ -4494,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." = "Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве."; @@ -4503,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" = "Настройки"; @@ -4514,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/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 93c56a730d..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,9 +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. */ "**e2e encrypted** audio call" = "การโทรเสียงแบบ **encrypted จากต้นจนจบ**"; @@ -62,16 +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 ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)"; +"**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 แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง"; +"**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"; @@ -109,9 +76,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ ได้รับการตรวจสอบแล้ว"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ เซิร์ฟเวอร์"; - /* notification title */ "%@ wants to connect!" = "%@ อยากเชื่อมต่อ!"; @@ -157,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 วินาที"; @@ -202,7 +163,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 วัน"; /* time interval */ @@ -211,10 +173,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. */ @@ -247,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 */ @@ -278,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." = "เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด"; @@ -407,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: %@" = "รุ่นแอป: %@"; @@ -527,7 +484,8 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; -/* No comment provided by engineer. */ +/* alert action +alert button */ "Cancel" = "ยกเลิก"; /* feature offered item */ @@ -536,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. */ @@ -567,7 +525,7 @@ "Change self-destruct mode" = "เปลี่ยนโหมดทําลายตัวเอง"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "เปลี่ยนรหัสผ่านแบบทำลายตัวเอง"; /* chat item text */ @@ -585,9 +543,6 @@ /* chat item text */ "changing address…" = "กำลังเปลี่ยนที่อยู่…"; -/* No comment provided by engineer. */ -"Chat archive" = "ที่เก็บแชทถาวร"; - /* No comment provided by engineer. */ "Chat console" = "คอนโซลแชท"; @@ -610,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. */ @@ -624,7 +582,7 @@ /* No comment provided by engineer. */ "Choose from library" = "เลือกจากอัลบั้ม"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "ลบ"; /* No comment provided by engineer. */ @@ -639,9 +597,6 @@ /* No comment provided by engineer. */ "colored" = "มีสี"; -/* No comment provided by engineer. */ -"Colors" = "สี"; - /* server test step */ "Compare file" = "เปรียบเทียบไฟล์"; @@ -705,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. */ @@ -747,9 +702,6 @@ /* notification */ "Contact is connected" = "เชื่อมต่อกับผู้ติดต่อแล้ว"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "ผู้ติดต่อยังไม่ได้เชื่อมต่อ!"; - /* No comment provided by engineer. */ "Contact name" = "ชื่อผู้ติดต่อ"; @@ -765,7 +717,7 @@ /* No comment provided by engineer. */ "Continue" = "ดำเนินการต่อ"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "คัดลอก"; /* No comment provided by engineer. */ @@ -774,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" = "สร้างไฟล์"; @@ -798,9 +747,6 @@ /* No comment provided by engineer. */ "Create your profile" = "สร้างโปรไฟล์ของคุณ"; -/* No comment provided by engineer. */ -"Created on %@" = "สร้างเมื่อ %@"; - /* No comment provided by engineer. */ "creator" = "ผู้สร้าง"; @@ -888,7 +834,8 @@ /* message decrypt error item */ "Decryption error" = "ข้อผิดพลาดในการ decrypt"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "ค่าเริ่มต้น (%@)"; /* No comment provided by engineer. */ @@ -897,7 +844,8 @@ /* No comment provided by engineer. */ "default (yes)" = "ค่าเริ่มต้น (ใช่)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "ลบ"; /* No comment provided by engineer. */ @@ -912,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" = "ลบโปรไฟล์แชท"; @@ -930,9 +872,6 @@ /* No comment provided by engineer. */ "Delete contact" = "ลบผู้ติดต่อ"; -/* No comment provided by engineer. */ -"Delete Contact" = "ลบผู้ติดต่อ"; - /* No comment provided by engineer. */ "Delete database" = "ลบฐานข้อมูล"; @@ -972,7 +911,7 @@ /* No comment provided by engineer. */ "Delete message?" = "ลบข้อความ?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "ลบข้อความ"; /* No comment provided by engineer. */ @@ -984,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?" = "ลบการเชื่อมต่อที่รอดำเนินการหรือไม่?"; @@ -1048,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)" = "ปิดใช้งาน (เก็บการแทนที่)"; @@ -1069,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" = "หายไปที่"; @@ -1125,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. */ @@ -1254,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)" = "เกิดข้อผิดพลาดในการเพิ่มสมาชิก"; @@ -1290,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" = "เกิดข้อผิดพลาดในการลบฐานข้อมูล"; @@ -1323,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" = "เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม"; @@ -1365,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. */ @@ -1386,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. */ @@ -1419,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. */ @@ -1441,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!" = "ไฟล์และสื่อต้องห้าม!"; @@ -1485,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!" = "ดำเนินการใหม่อย่างสมบูรณ์ - ทำงานในพื้นหลัง!"; @@ -1527,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. (24 hours)" = "สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร"; - -/* 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:" = "ข้อความกลุ่ม:"; @@ -1602,9 +1507,6 @@ /* time unit */ "hours" = "ชั่วโมง"; -/* No comment provided by engineer. */ -"How it works" = "มันทำงานอย่างไร"; - /* No comment provided by engineer. */ "How SimpleX works" = "วิธีการ SimpleX ทํางานอย่างไร"; @@ -1645,7 +1547,7 @@ "Immediately" = "โดยทันที"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "มีภูมิคุ้มกันต่อสแปมและการละเมิด"; +"Immune to spam" = "มีภูมิคุ้มกันต่อสแปมและการละเมิด"; /* No comment provided by engineer. */ "Import" = "นำเข้า"; @@ -1711,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" = "อินเตอร์เฟซ"; @@ -1731,7 +1633,7 @@ /* invalid chat item */ "invalid data" = "ข้อมูลไม่ถูกต้อง"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!"; /* No comment provided by engineer. */ @@ -1774,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." = "อนุญาตให้มีการเชื่อมต่อที่ไม่ระบุตัวตนจำนวนมากโดยไม่มีข้อมูลที่ใช้ร่วมกันระหว่างกันในโปรไฟล์การแชทเดียว"; @@ -1797,7 +1699,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "อินเทอร์เฟซภาษาญี่ปุ่น"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "เข้าร่วม"; /* No comment provided by engineer. */ @@ -1827,7 +1729,7 @@ /* No comment provided by engineer. */ "Learn more" = "ศึกษาเพิ่มเติม"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "ออกจาก"; /* No comment provided by engineer. */ @@ -1857,9 +1759,6 @@ /* No comment provided by engineer. */ "Live messages" = "ข้อความสด"; -/* No comment provided by engineer. */ -"Local" = "ในเครื่อง"; - /* No comment provided by engineer. */ "Local name" = "ชื่อภายในเครื่องเท่านั้น"; @@ -1872,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" = "ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน"; @@ -1926,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" = "ข้อผิดพลาดในการส่งข้อความ"; @@ -1942,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" = "ข้อความที่ได้รับ"; @@ -1969,7 +1877,7 @@ "Migration is completed" = "การโยกย้ายเสร็จสมบูรณ์"; /* No comment provided by engineer. */ -"Migrations: %@" = "การย้ายข้อมูล: %@"; +"Migrations:" = "การย้ายข้อมูล"; /* time unit */ "minutes" = "นาที"; @@ -1998,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. */ @@ -2022,7 +1927,7 @@ /* No comment provided by engineer. */ "Network status" = "สถานะเครือข่าย"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "ไม่เคย"; /* notification */ @@ -2031,9 +1936,6 @@ /* notification */ "New contact:" = "คำขอติดต่อใหม่:"; -/* No comment provided by engineer. */ -"New database archive" = "ฐานข้อมูลใหม่สำหรับการเก็บถาวร"; - /* No comment provided by engineer. */ "New display name" = "ชื่อที่แสดงใหม่"; @@ -2088,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" = "การแจ้งเตือน"; @@ -2107,11 +2015,11 @@ "observer" = "ผู้สังเกตการณ์"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "ปิด"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "ปิด"; /* feature offered item */ @@ -2120,15 +2028,12 @@ /* 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" = "เปิด"; @@ -2136,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." = "เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้"; @@ -2195,12 +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." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; - /* member role */ "owner" = "เจ้าของ"; @@ -2229,10 +2128,7 @@ "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" = "ข้อผิดพลาดในการถอดรหัสอย่างถาวร"; @@ -2288,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" = "ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; @@ -2315,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. */ @@ -2363,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)"; @@ -2378,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…" = "ได้รับคำตอบ…"; @@ -2426,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. */ @@ -2507,9 +2398,6 @@ /* chat item action */ "Reveal" = "เปิดเผย"; -/* No comment provided by engineer. */ -"Revert" = "เปลี่ยนกลับ"; - /* No comment provided by engineer. */ "Revoke" = "ถอน"; @@ -2525,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. */ @@ -2540,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" = "บันทึกโปรไฟล์กลุ่ม"; @@ -2555,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. */ @@ -2564,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?" = "บันทึกข้อความต้อนรับ?"; @@ -2612,7 +2492,7 @@ /* chat item text */ "security code changed" = "เปลี่ยนรหัสความปลอดภัยแล้ว"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "เลือก"; /* No comment provided by engineer. */ @@ -2636,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" = "ส่งข้อความแบบที่หายไป"; @@ -2651,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" = "ส่งคําถามและความคิด"; @@ -2663,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. */ @@ -2741,7 +2615,8 @@ /* No comment provided by engineer. */ "Settings" = "การตั้งค่า"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "แชร์"; /* No comment provided by engineer. */ @@ -2750,7 +2625,7 @@ /* No comment provided by engineer. */ "Share address" = "แชร์ที่อยู่"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "แชร์ที่อยู่กับผู้ติดต่อ?"; /* No comment provided by engineer. */ @@ -2813,9 +2688,6 @@ /* 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"; @@ -2834,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." = "หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง"; @@ -2852,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 */ @@ -2891,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"; @@ -2915,7 +2781,7 @@ /* No comment provided by engineer. */ "Test servers" = "เซิร์ฟเวอร์ทดสอบ"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "การทดสอบล้มเหลว!"; /* No comment provided by engineer. */ @@ -2927,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." = "แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน"; @@ -2948,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." = "แฮชของข้อความก่อนหน้านี้แตกต่างกัน"; @@ -2960,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! ✅" = "ขีดที่สองที่เราพลาด! ✅"; @@ -2978,9 +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. */ "These settings are for your current profile **%@**." = "การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ **%@**"; @@ -3011,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." = "ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน"; @@ -3050,13 +2910,10 @@ /* 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. */ @@ -3095,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" = "อัปเกรดและเปิดการแชท"; @@ -3149,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 อยู่"; @@ -3210,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!" = "ห้ามข้อความเสียง!"; @@ -3254,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." = "เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ"; @@ -3269,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" = "คุณยอมรับการเชื่อมต่อ"; @@ -3318,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." = "คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า"; @@ -3329,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" = "คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป"; @@ -3356,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." = "คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์"; @@ -3428,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" = "การโทรของคุณ"; @@ -3443,9 +3267,6 @@ /* 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 (%@)." = "ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)"; @@ -3471,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." = "โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ"; @@ -3479,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" = "ที่อยู่เซิร์ฟเวอร์ของคุณ"; @@ -3491,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 index 714a1bc739..e3bb11d1cc 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.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)" = " (kopyalanabilir)"; @@ -31,30 +16,15 @@ /* 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. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 renklendirilmiş!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(yeni)"; /* No comment provided by engineer. */ "(this device v%@)" = "(bu cihaz v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* 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)"; @@ -65,10 +35,7 @@ "[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. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**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. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Yeni kişi ekleyin**: tek seferlik QR Kodunuzu oluşturmak veya kişisel ulaşım bilgileri bağlantısı için."; +"**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."; @@ -80,10 +47,10 @@ "**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. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**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."; +"**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 notifications server, check messages periodically in the background (depends 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)."; +"**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."; @@ -92,7 +59,10 @@ "**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 notifications are sent to SimpleX Chat notification server, but not 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."; +"**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."; @@ -155,7 +125,10 @@ "%@ is verified" = "%@ onaylandı"; /* No comment provided by engineer. */ -"%@ servers" = "%@ sunucuları"; +"%@ server" = "%@ sunucu"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ sunucular"; /* No comment provided by engineer. */ "%@ uploaded" = "%@ yüklendi"; @@ -163,6 +136,9 @@ /* 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"; @@ -175,9 +151,24 @@ /* 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"; @@ -229,9 +220,6 @@ /* No comment provided by engineer. */ "%lld new interface languages" = "%lld yeni arayüz dilleri"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld saniye"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld saniye"; @@ -275,9 +263,10 @@ "0 sec" = "0 saniye"; /* No comment provided by engineer. */ -"0s" = "0 saniye"; +"0s" = "0sn"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 gün"; /* time interval */ @@ -286,12 +275,20 @@ /* No comment provided by engineer. */ "1 minute" = "1 dakika"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 ay"; -/* time interval */ +/* 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"; @@ -326,10 +323,7 @@ "Abort changing address?" = "Adres değişimi iptal edilsin mi?"; /* No comment provided by engineer. */ -"About SimpleX" = "SimpleX Hakkında"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "SimpleX Chat adresi hakkında"; +"About operators" = "Operatörler hakkında"; /* No comment provided by engineer. */ "About SimpleX Chat" = "SimpleX Chat hakkında"; @@ -338,54 +332,95 @@ "above, then choose:" = "yukarı çıkın, ardından seçin:"; /* No comment provided by engineer. */ -"Accent color" = "Vurgu rengi"; +"Accent" = "Ana renk"; /* accept contact request via notification - accept incoming call 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 */ +/* 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 contact" = "Kişi ekle"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Önceden ayarlanmış sunucu ekle"; +"Add friends" = "Arkadaş ekle"; /* No comment provided by engineer. */ "Add profile" = "Profil ekle"; /* No comment provided by engineer. */ -"Add server…" = "Sunucu ekle…"; +"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"; @@ -401,6 +436,9 @@ /* 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…"; @@ -416,12 +454,18 @@ /* 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!"; @@ -431,6 +475,9 @@ /* 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."; @@ -438,7 +485,7 @@ "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, sohbetleriniz ve dosyalarınız güvenli bir şekilde şifrelenecek ve parçalar halinde yapılandırılmış XFTP rölelerine yüklenecektir."; +"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"; @@ -446,9 +493,15 @@ /* 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)"; @@ -459,11 +512,14 @@ "Allow message reactions." = "Mesaj tepkilerine izin ver."; /* No comment provided by engineer. */ -"Allow sending direct messages to members." = "Üyelere direkt mesaj göndermeye izin ver."; +"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)"; @@ -509,6 +565,9 @@ /* 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"; @@ -521,6 +580,9 @@ /* 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ü: %@"; @@ -539,6 +601,9 @@ /* 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ü"; @@ -551,15 +616,27 @@ /* 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"; @@ -599,9 +676,15 @@ /* 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"; @@ -617,12 +700,33 @@ /* 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"; @@ -647,12 +751,19 @@ /* rcv group event chat item */ "blocked %@" = "engellendi %@"; -/* marked deleted chat item preview text */ +/* 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"; @@ -674,9 +785,18 @@ /* 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!"; @@ -692,9 +812,18 @@ /* 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!"; @@ -702,6 +831,10 @@ "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. */ @@ -714,14 +847,23 @@ "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?"; @@ -747,7 +889,7 @@ "Change self-destruct mode" = "Kendini yok etme modunu değiştir"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Kendini yok eden parolayı değiştir"; /* chat item text */ @@ -766,7 +908,16 @@ "changing address…" = "adres değiştiriliyor…"; /* No comment provided by engineer. */ -"Chat archive" = "Sohbet arşivi"; +"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"; @@ -777,6 +928,9 @@ /* 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ı"; @@ -789,16 +943,40 @@ /* 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. */ @@ -814,6 +992,15 @@ "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. */ @@ -829,10 +1016,13 @@ "Clear verification" = "Doğrulamayı temizle"; /* No comment provided by engineer. */ -"colored" = "renklendirilmiş"; +"Color chats with the new themes." = "Yeni temalarla renkli sohbetler."; /* No comment provided by engineer. */ -"Colors" = "Renkler"; +"Color mode" = "Renk modu"; + +/* No comment provided by engineer. */ +"colored" = "renklendirilmiş"; /* server test step */ "Compare file" = "Dosya karşılaştır"; @@ -843,15 +1033,45 @@ /* 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"; @@ -885,6 +1105,9 @@ /* 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?"; @@ -909,6 +1132,9 @@ /* 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ı"; @@ -916,11 +1142,17 @@ "connected directly" = "doğrudan bağlandı"; /* No comment provided by engineer. */ -"Connected to desktop" = "Bilgisayara bağlanıldı"; +"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ı)"; @@ -942,15 +1174,21 @@ /* 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"; -/* chat list item title */ +/* 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ı"; @@ -960,18 +1198,30 @@ /* 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"; @@ -981,6 +1231,9 @@ /* 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"; @@ -994,7 +1247,7 @@ "Contact is connected" = "Kişi bağlandı"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Kişi şuan bağlanmadı!"; +"Contact is deleted." = "Kişi silindi."; /* No comment provided by engineer. */ "Contact name" = "Kişi adı"; @@ -1002,6 +1255,9 @@ /* 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"; @@ -1011,12 +1267,21 @@ /* No comment provided by engineer. */ "Continue" = "Devam et"; -/* chat item action */ +/* 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?"; @@ -1024,10 +1289,10 @@ "Create" = "Oluştur"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Rasgele profil kullanarak grup oluştur."; +"Create 1-time link" = "Tek kullanımlık bağlantı oluştur"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "İnsanların seninle bağlanması için bir adres oluştur."; +"Create a group using a random profile." = "Rasgele profil kullanarak grup oluştur."; /* server test step */ "Create file" = "Dosya oluştur"; @@ -1059,15 +1324,15 @@ /* 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. */ -"Created on %@" = "%@ de oluşturuldu"; - /* No comment provided by engineer. */ "Creating archive link" = "Arşiv bağlantısı oluşturuluyor"; @@ -1077,12 +1342,18 @@ /* 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."; @@ -1092,9 +1363,18 @@ /* 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"; @@ -1155,13 +1435,20 @@ /* 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ı"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "Şifre çözme hataları"; + +/* delete after time +pref value */ "default (%@)" = "varsayılan (%@)"; /* No comment provided by engineer. */ @@ -1170,9 +1457,13 @@ /* No comment provided by engineer. */ "default (yes)" = "varsayılan (evet)"; -/* chat item action */ +/* 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?"; @@ -1192,10 +1483,7 @@ "Delete and notify contact" = "Sil ve kişiye bildir"; /* No comment provided by engineer. */ -"Delete archive" = "Arşivi sil"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Sohbet arşivi silinsin mi?"; +"Delete chat" = "Sohbeti sil"; /* No comment provided by engineer. */ "Delete chat profile" = "Sohbet profilini sil"; @@ -1203,6 +1491,9 @@ /* 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"; @@ -1210,10 +1501,7 @@ "Delete contact" = "Kişiyi sil"; /* No comment provided by engineer. */ -"Delete Contact" = "Kişiyi sil"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Kişi silinsin mi?\nBu geri alınamaz!"; +"Delete contact?" = "Kişiyi sil?"; /* No comment provided by engineer. */ "Delete database" = "Veritabanını sil"; @@ -1257,7 +1545,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Mesaj silinsin mi?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Mesajları sil"; /* No comment provided by engineer. */ @@ -1270,7 +1558,7 @@ "Delete old database?" = "Eski veritabanı silinsin mi?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Bekleyen bağlantıyı sil"; +"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?"; @@ -1281,12 +1569,21 @@ /* 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"; @@ -1299,6 +1596,9 @@ /* 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"; @@ -1320,9 +1620,27 @@ /* 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ı"; @@ -1348,7 +1666,10 @@ "Direct messages" = "Doğrudan mesajlar"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Bu grupta üyeler arasında direkt mesajlaşma yasaktır."; +"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)"; @@ -1362,6 +1683,9 @@ /* 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"; @@ -1372,7 +1696,7 @@ "Disappearing messages are prohibited in this chat." = "Kaybolan mesajlar bu sohbette yasaklanmış."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Kaybolan mesajlar bu grupta yasaklanmış."; +"Disappearing messages are prohibited." = "Kaybolan mesajlar bu grupta yasaklanmış."; /* No comment provided by engineer. */ "Disappears at" = "da kaybolur"; @@ -1398,6 +1722,15 @@ /* 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."; @@ -1413,15 +1746,28 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Sürüm düşür ve sohbeti aç"; -/* chat item action */ +/* 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"; @@ -1434,12 +1780,18 @@ /* 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"; @@ -1452,7 +1804,7 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Etkinleştir (geçersiz kılmaları koru)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Otomatik mesaj silme etkinleştirilsin mi?"; /* No comment provided by engineer. */ @@ -1491,6 +1843,9 @@ /* enabled status */ "enabled" = "etkin"; +/* No comment provided by engineer. */ +"Enabled" = "Etkin"; + /* No comment provided by engineer. */ "Enabled for" = "Şunlar için etkinleştirildi"; @@ -1614,24 +1969,36 @@ /* 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 accessing database file" = "Veritabanı dosyasına erişilirken 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"; @@ -1662,9 +2029,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Bağlantı silinirken hata oluştu"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Kişi silinirken hata oluştu"; - /* No comment provided by engineer. */ "Error deleting database" = "Veritabanı silinirken hata oluştu"; @@ -1692,26 +2056,38 @@ /* 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 loading %@ servers" = "%@ sunucuları yüklenirken hata oluştu"; +"Error migrating settings" = "Ayarlar taşınırken hata oluştu"; /* No comment provided by engineer. */ "Error opening chat" = "Sohbeti açarken sorun oluştu"; -/* No comment provided by engineer. */ +/* 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 saving %@ servers" = "%@ sunucuları kaydedilirken sorun oluştu"; +"Error resetting statistics" = "Hata istatistikler sıfırlanıyor"; /* No comment provided by engineer. */ "Error saving group profile" = "Grup profili kaydedilirken sorun oluştu"; @@ -1725,6 +2101,9 @@ /* 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"; @@ -1753,6 +2132,9 @@ "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. */ @@ -1764,6 +2146,9 @@ /* 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"; @@ -1779,7 +2164,9 @@ /* No comment provided by engineer. */ "Error: " = "Hata: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Hata: %@"; /* No comment provided by engineer. */ @@ -1789,10 +2176,13 @@ "Error: URL is invalid" = "Hata: URL geçersiz"; /* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Konuşma sırasında devre dışı bırakılsa bile."; +"Errors" = "Hatalar"; + +/* servers error */ +"Errors in servers configuration." = "Sunucular yapılandırılırken hatalar oluştu."; /* No comment provided by engineer. */ -"event happened" = "etkinlik yaşandı"; +"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"; @@ -1800,12 +2190,18 @@ /* 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."; @@ -1824,9 +2220,27 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Daha hızlı katılma ve daha güvenilir mesajlar."; -/* No comment provided by engineer. */ +/* 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."; @@ -1839,6 +2253,9 @@ /* No comment provided by engineer. */ "File: %@" = "Dosya: %@"; +/* No comment provided by engineer. */ +"Files" = "Dosyalar"; + /* No comment provided by engineer. */ "Files & media" = "Dosyalar & medya"; @@ -1846,7 +2263,7 @@ "Files and media" = "Dosyalar ve medya"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Dosyalar ve medya bu grupta yasaklandı."; +"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"; @@ -1887,15 +2304,39 @@ /* 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"; @@ -1905,6 +2346,24 @@ /* 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"; @@ -1917,9 +2376,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Bütün isim (opsiyonel)"; -/* No comment provided by engineer. */ -"Full name:" = "Bütün isim:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Tamamiyle merkezi olmayan - sadece kişilere görünür."; @@ -1932,6 +2388,12 @@ /* 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"; @@ -1968,27 +2430,6 @@ /* No comment provided by engineer. */ "Group links" = "Grup bağlantıları"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Grup üyeleri mesaj tepkileri ekleyebilir."; - -/* No comment provided by engineer. */ -"Group 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. */ -"Group members can send direct messages." = "Grup üyeleri doğrudan mesajlar gönderebilir."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Grup üyeleri kaybolan mesajlar gönderebilir."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Grup üyeleri dosyalar ve medya gönderebilir."; - -/* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Grup üyeleri SimpleX bağlantıları gönderebilir."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Grup üyeleri sesli mesajlar gönderebilir."; - /* notification */ "Group message:" = "Grup mesajı:"; @@ -2050,7 +2491,10 @@ "hours" = "saat"; /* No comment provided by engineer. */ -"How it works" = "Nasıl çalışıyor"; +"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"; @@ -2095,7 +2539,7 @@ "Immediately" = "Hemen"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Spam ve kötüye kullanıma karşı bağışıklı"; +"Immune to spam" = "Spam ve kötüye kullanıma karşı bağışıklı"; /* No comment provided by engineer. */ "Import" = "İçe aktar"; @@ -2109,9 +2553,15 @@ /* 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"; @@ -2130,6 +2580,9 @@ /* 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"; @@ -2184,14 +2637,17 @@ /* 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. */ -"Instantly" = "Anında"; +"Interface" = "Arayüz"; /* No comment provided by engineer. */ -"Interface" = "Arayüz"; +"Interface colors" = "Arayüz renkleri"; /* invalid chat data */ "invalid chat" = "geçersi̇z sohbet"; @@ -2223,7 +2679,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Geçersiz yanıt"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Geçersiz sunucu adresi!"; /* item status text */ @@ -2235,12 +2691,18 @@ /* 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"; @@ -2262,6 +2724,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 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"; @@ -2269,7 +2734,7 @@ "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 in this group." = "Bu grupta geri döndürülemez mesaj silme yasaktır."; +"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."; @@ -2280,6 +2745,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." = "Ş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 (%@)."; @@ -2292,7 +2760,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japonca arayüz"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Katıl"; /* No comment provided by engineer. */ @@ -2319,13 +2787,16 @@ /* No comment provided by engineer. */ "Joining group" = "Gruba katılınıyor"; -/* No comment provided by engineer. */ +/* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Kullanılmamış davet tutulsun mu?"; /* No comment provided by engineer. */ @@ -2343,9 +2814,15 @@ /* No comment provided by engineer. */ "Learn more" = "Daha fazlası"; -/* No comment provided by engineer. */ +/* 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"; @@ -2382,9 +2859,6 @@ /* No comment provided by engineer. */ "Live messages" = "Canlı mesajlar"; -/* No comment provided by engineer. */ -"Local" = "Yerel"; - /* No comment provided by engineer. */ "Local name" = "Yerel isim"; @@ -2397,24 +2871,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Kilit modu"; -/* No comment provided by engineer. */ -"Make a private connection" = "Gizli bir bağlantı oluştur"; - /* 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 %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "%@ sunucu adreslerinin doğru formatta olduğundan, satır ayrımı yapıldığından ve yinelenmediğinden (%@) emin olun."; - /* 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. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Çoğu kişi sordu: *eğer SimpleX'in hiç kullanıcı tanımlayıcıları yok, o zaman mesajları nasıl gönderebiliyor?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Herkes için silinmiş olarak işaretle"; @@ -2433,6 +2898,12 @@ /* 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"; @@ -2445,6 +2916,12 @@ /* 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."; @@ -2454,15 +2931,54 @@ /* 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"; @@ -2470,14 +2986,29 @@ "Message reactions are prohibited in this chat." = "Mesaj tepkileri bu sohbette yasaklandı."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Mesaj tepkileri bu grupta yasaklandı."; +"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ı"; @@ -2493,6 +3024,15 @@ /* 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."; @@ -2533,7 +3073,7 @@ "Migration is completed" = "Geçiş tamamlandı"; /* No comment provided by engineer. */ -"Migrations: %@" = "Geçişler: %@"; +"Migrations:" = "Geçişler:"; /* time unit */ "minutes" = "dakikalar"; @@ -2568,19 +3108,16 @@ /* item status description */ "Most likely this connection is deleted." = "Büyük ihtimalle bu bağlantı silinmiş."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Büyük ihtimalle bu kişi seninle bağlantını sildi."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Çoklu sohbet profili"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Sustur"; /* No comment provided by engineer. */ "Muted when inactive!" = "Aktif değilken susturuldu!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "İsim"; /* No comment provided by engineer. */ @@ -2589,6 +3126,9 @@ /* 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"; @@ -2598,21 +3138,21 @@ /* No comment provided by engineer. */ "Network status" = "Ağ durumu"; -/* No comment provided by engineer. */ +/* 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 database archive" = "Yeni veritabanı arşivi"; - /* No comment provided by engineer. */ "New desktop app!" = "Yeni bilgisayar uygulaması!"; @@ -2622,6 +3162,9 @@ /* 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ü"; @@ -2637,6 +3180,12 @@ /* 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"; @@ -2658,6 +3207,9 @@ /* 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"; @@ -2670,21 +3222,42 @@ /* 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"; @@ -2698,11 +3271,11 @@ "observer" = "gözlemci"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "kapalı"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Kapalı"; /* feature offered item */ @@ -2711,7 +3284,7 @@ /* feature offered item */ "offered %@: %@" = "%1$@: %2$@ teklif etti"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Tamam"; /* No comment provided by engineer. */ @@ -2720,9 +3293,6 @@ /* No comment provided by engineer. */ "Old database" = "Eski veritabanı"; -/* No comment provided by engineer. */ -"Old database archive" = "Eski veritabanı arşivi"; - /* group pref value */ "on" = "açık"; @@ -2730,16 +3300,19 @@ "One-time invitation link" = "Tek zamanlı bağlantı daveti"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Bağlantı için Onion ana bilgisayarları gerekecektir. VPN'nin etkinleştirilmesi gerekir."; +"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. Requires enabling VPN." = "Onion ana bilgisayarları mevcutsa kullanılacaktır. VPN'nin etkinleştirilmesi gerekir."; +"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 sent with **2-layer end-to-end encryption**." = "Yalnızca istemci cihazlar kullanıcı profillerini, kişileri, grupları ve **2 katmanlı uçtan uca şifreleme** ile gönderilen mesajları depolar."; +"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."; @@ -2780,7 +3353,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Sadece karşıdaki kişi sesli mesajlar gönderebilir."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Aç"; /* No comment provided by engineer. */ @@ -2798,12 +3371,6 @@ /* No comment provided by engineer. */ "Open Settings" = "Ayarları aç"; -/* authentication reason */ -"Open user profiles" = "Kullanıcı profillerini aç"; - -/* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir."; - /* No comment provided by engineer. */ "Opening app…" = "Uygulama açılıyor…"; @@ -2819,9 +3386,18 @@ /* 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"; @@ -2843,6 +3419,9 @@ /* 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"; @@ -2865,10 +3444,10 @@ "peer-to-peer" = "eşler arası"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "İnsanlar size yalnızca paylaştığınız bağlantılar üzerinden ulaşabilir."; +"Pending" = "Bekleniyor"; /* No comment provided by engineer. */ -"Periodically" = "Periyodik olarak"; +"Periodic" = "Periyodik olarak"; /* message decrypt error item */ "Permanent decryption error" = "Kalıcı şifre çözümü hatası"; @@ -2882,9 +3461,18 @@ /* 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."; @@ -2927,21 +3515,24 @@ /* 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" = "Ön ayarlı sunucu"; - /* 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"; @@ -2951,9 +3542,21 @@ /* 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ı"; @@ -2963,16 +3566,13 @@ /* No comment provided by engineer. */ "Profile images" = "Profil resimleri"; -/* No comment provided by engineer. */ -"Profile name" = "Profil ismi"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profil ismi:"; - /* 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. */ @@ -2988,7 +3588,7 @@ "Prohibit messages reactions." = "Mesajlarda tepkileri yasakla."; /* No comment provided by engineer. */ -"Prohibit sending direct messages to members." = "Geri dönülmez mesaj silme işlemini yasakla."; +"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."; @@ -3005,15 +3605,30 @@ /* 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"; @@ -3029,20 +3644,23 @@ /* 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…"; -/* No comment provided by engineer. */ +/* 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/app-settings.html#your-simplex-contact-address)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(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)." = "[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/chat-profiles.html#incognito-mode)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](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)."; @@ -3051,10 +3669,10 @@ "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. */ -"Read more in our GitHub repository." = "Daha fazlasını GitHub depomuzdan oku."; +"Receipts are disabled" = "Alıcılar devre dışı bırakıldı"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Gönderildi bilgisi devre dışı bırakıldı"; +"Receive errors" = "Alım sırasında hata"; /* No comment provided by engineer. */ "received answer…" = "alınan cevap…"; @@ -3075,10 +3693,16 @@ "Received message" = "Mesaj alındı"; /* 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."; +"Received messages" = "Alınan mesajlar"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Eşzamanlılık alınıyor"; +"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."; @@ -3095,9 +3719,24 @@ /* 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ı?"; @@ -3110,7 +3749,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Azaltılmış pil kullanımı"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "Reddet"; /* No comment provided by engineer. */ @@ -3123,14 +3763,20 @@ "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." = "Aktarma sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir."; +"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." = "Aktarıcı sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir."; +"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"; @@ -3188,12 +3834,27 @@ /* 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"; @@ -3218,9 +3879,6 @@ /* chat item action */ "Reveal" = "Göster"; -/* No comment provided by engineer. */ -"Revert" = "Geri al"; - /* No comment provided by engineer. */ "Revoke" = "İptal et"; @@ -3236,30 +3894,31 @@ /* 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"; -/* chat item action */ +/* alert button +chat item action */ "Save" = "Kaydet"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Kaydet (ve kişilere bildir)"; -/* No comment provided by engineer. */ +/* 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 archive" = "Arşivi kaydet"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Otomatik kabul et ayarlarını kaydet"; - /* No comment provided by engineer. */ "Save group profile" = "Grup profilini kaydet"; @@ -3269,7 +3928,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Parolayı Anahtar Zincirinde kaydet"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Tercihler kaydedilsin mi?"; /* No comment provided by engineer. */ @@ -3278,15 +3937,15 @@ /* No comment provided by engineer. */ "Save servers" = "Sunucuları kaydet"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Sunucular kaydedilsin mi?"; -/* No comment provided by engineer. */ -"Save settings?" = "Ayarlar 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"; @@ -3305,6 +3964,15 @@ /* 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"; @@ -3320,6 +3988,9 @@ /* 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"; @@ -3332,6 +4003,9 @@ /* network option */ "sec" = "sn"; +/* No comment provided by engineer. */ +"Secondary" = "İkincil renk"; + /* time unit */ "seconds" = "saniye"; @@ -3341,6 +4015,9 @@ /* 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"; @@ -3350,9 +4027,18 @@ /* chat item text */ "security code changed" = "güvenlik kodu değiştirildi"; -/* No comment provided by engineer. */ +/* 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"; @@ -3377,15 +4063,15 @@ /* No comment provided by engineer. */ "send direct message" = "doğrudan mesaj 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"; @@ -3393,10 +4079,16 @@ "Send live message" = "Canlı mesaj gönder"; /* No comment provided by engineer. */ -"Send notifications" = "Bildirimler gönder"; +"Send message to enable calls." = "Çağrıları aktif etmek için mesaj gönder."; /* No comment provided by engineer. */ -"Send notifications:" = "Bildirimler gönder:"; +"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"; @@ -3410,7 +4102,7 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Yeni üyelere 100 adete kadar son mesajları gönderin."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Gönderici dosya gönderimini iptal etti."; /* No comment provided by engineer. */ @@ -3420,7 +4112,7 @@ "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." = "Gönderildi bilgisi tüm kişiler için etkinleştirilecektir."; +"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."; @@ -3446,15 +4138,45 @@ /* 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"; @@ -3464,9 +4186,24 @@ /* 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"; @@ -3476,6 +4213,9 @@ /* 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"; @@ -3506,10 +4246,14 @@ /* 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"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "Paylaş"; /* No comment provided by engineer. */ @@ -3518,18 +4262,30 @@ /* No comment provided by engineer. */ "Share address" = "Adresi paylaş"; -/* No comment provided by engineer. */ +/* 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"; @@ -3539,6 +4295,12 @@ /* 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"; @@ -3548,6 +4310,9 @@ /* No comment provided by engineer. */ "Show:" = "Göster:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX adresi"; @@ -3570,7 +4335,7 @@ "SimpleX links" = "SimpleX bağlantıları"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "SimpleX bağlantıları bu grupta yasaklandı."; +"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"; @@ -3590,9 +4355,15 @@ /* 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"; @@ -3603,11 +4374,26 @@ "Small groups (max 20)" = "Küçük gruplar (en fazla 20 kişi)"; /* No comment provided by engineer. */ -"SMP servers" = "SMP sunucuları"; +"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"; @@ -3626,18 +4412,21 @@ /* 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 enable database actions" = "Veritabanı eylemlerini etkinleştirmek için sohbeti durdur"; - /* 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."; @@ -3653,10 +4442,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Dosya gönderimi durdurulsun mu?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Paylaşmayı durdur"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Adresi paylaşmak durdurulsun mu?"; /* authentication reason */ @@ -3668,18 +4457,39 @@ /* 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"; @@ -3705,7 +4515,7 @@ "Tap to scan" = "Taramak için tıkla"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Yeni bir sohbet başlatmak için tıkla"; +"TCP connection" = "TCP bağlantısı"; /* No comment provided by engineer. */ "TCP connection timeout" = "TCP bağlantı zaman aşımı"; @@ -3719,6 +4529,9 @@ /* 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."; @@ -3728,7 +4541,7 @@ /* No comment provided by engineer. */ "Test servers" = "Sunucuları test et"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testler başarısız oldu!"; /* No comment provided by engineer. */ @@ -3741,10 +4554,10 @@ "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 1st platform without any user identifiers – private by design." = "Herhangi bir kullanıcı tanımlayıcısı olmayan ilk platform - tasarım gereği gizli."; +"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 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."; +"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ı."; @@ -3764,6 +4577,9 @@ /* 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ı."; @@ -3777,13 +4593,16 @@ "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 next generation of private messaging" = "Gizli mesajlaşmanın yeni nesli"; +"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. */ -"The profile is only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak."; +"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! ✅"; @@ -3798,7 +4617,10 @@ "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. */ -"Theme" = "Tema"; +"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."; @@ -3842,9 +4664,15 @@ /* 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:"; @@ -3857,15 +4685,24 @@ /* 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 privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate 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 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."; @@ -3878,12 +4715,24 @@ /* 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: %@)."; @@ -3920,13 +4769,10 @@ /* rcv group event chat item */ "unblocked %@" = "engeli kaldırıldı %@"; -/* item status description */ -"Unexpected error: %@" = "Beklenmeyen hata: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Beklenmeyen geçiş durumu"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Favorilerden çık."; /* No comment provided by engineer. */ @@ -3953,6 +4799,12 @@ /* 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"; @@ -3974,10 +4826,13 @@ /* authentication reason */ "Unlock app" = "Uygulama kilidini aç"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Susturmayı kaldır"; /* No comment provided by engineer. */ +"unprotected" = "korumasız"; + +/* swipe action */ "Unread" = "Okunmamış"; /* No comment provided by engineer. */ @@ -3986,9 +4841,6 @@ /* No comment provided by engineer. */ "Update" = "Güncelle"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = ".onion ana bilgisayarların ayarı güncellensin mi?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Veritabanı parolasını güncelle"; @@ -3996,7 +4848,7 @@ "Update network settings?" = "Bağlantı ayarları güncellensin mi?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Taşıma izolasyon modu güncellensin mi?"; +"Update settings?" = "Ayarları güncelleyelim mi?"; /* rcv group event chat item */ "updated group profile" = "grup profili güncellendi"; @@ -4008,10 +4860,10 @@ "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. */ -"Updating this setting will re-connect the client to all servers." = "Bu ayarın güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır."; +"Upgrade and open chat" = "Yükselt ve sohbeti aç"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Yükselt ve sohbeti aç"; +"Upload errors" = "Yükleme hataları"; /* No comment provided by engineer. */ "Upload failed" = "Yükleme başarısız"; @@ -4019,6 +4871,12 @@ /* 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"; @@ -4046,20 +4904,32 @@ /* 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. */ -"User profile" = "Kullanıcı profili"; +"Use the app with one hand." = "Uygulamayı tek elle kullan."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = ".onion ana bilgisayarlarını kullanmak için uyumlu VPN sağlayıcısı gerekir."; +"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."; @@ -4109,6 +4979,9 @@ /* 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"; @@ -4140,7 +5013,7 @@ "Voice messages are prohibited in this chat." = "Bu sohbette sesli mesajlar yasaktır."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Bu grupta sesli mesajlar yasaktır."; +"Voice messages are prohibited." = "Bu grupta sesli mesajlar yasaktır."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Sesli mesajlara izin verilmiyor"; @@ -4166,6 +5039,12 @@ /* 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!"; @@ -4200,7 +5079,7 @@ "When connecting audio and video calls." = "Sesli ve görüntülü aramalara bağlanırken."; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "İnsanlar bağlantı talebinde bulunduğunda, kabul edebilir veya reddedebilirsiniz."; +"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."; @@ -4223,14 +5102,26 @@ /* 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 servers" = "XFTP sunucuları"; +"XFTP server" = "XFTP sunucusu"; /* pref value */ "yes" = "evet"; @@ -4238,9 +5129,6 @@ /* No comment provided by engineer. */ "you" = "sen"; -/* 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**."; @@ -4286,6 +5174,9 @@ /* 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"; @@ -4295,6 +5186,9 @@ /* 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"; @@ -4314,7 +5208,10 @@ "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 send messages to %@" = "Artık %@ adresine mesaj gönderebilirsin"; +"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."; @@ -4326,10 +5223,10 @@ "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 share your address as a link or QR code - anybody can connect to you." = "Adresinizi bir bağlantı veya QR kodu olarak paylaşabilirsiniz - herkes size bağlanabilir."; +"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 start chat via app Settings / Database or by restarting the app" = "Sohbeti uygulamada Ayarlar / Veritabanı üzerinden veya uygulamayı yeniden başlatarak başlatabilirsiniz"; +"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."; @@ -4337,7 +5234,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Mesajları biçimlendirmek için markdown kullanabilirsiniz:"; -/* No comment provided by engineer. */ +/* 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. */ @@ -4356,10 +5253,10 @@ "you changed role of %@ to %@" = "%1$@'in yetkisini %2$@ olarak değiştirdiniz"; /* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Mesajların hangi sunucu(lar)dan **alınacağını**, kişilerinizi - onlara mesaj göndermek için kullandığınız sunucuları - siz kontrol edersiniz."; +"You could not be verified; please try again." = "Doğrulanamadınız; lütfen tekrar deneyin."; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Doğrulanamadınız; lütfen tekrar deneyin."; +"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!"; @@ -4367,9 +5264,6 @@ /* 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 no chats" = "Hiç sohbetiniz yok"; - /* 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."; @@ -4385,9 +5279,18 @@ /* 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."; @@ -4445,9 +5348,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" = "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 %@ servers" = "%@ sunucularınız"; - /* No comment provided by engineer. */ "Your calls" = "Aramaların"; @@ -4457,11 +5357,14 @@ /* 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 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)." = "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)."; +"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."; @@ -4472,6 +5375,9 @@ /* 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."; @@ -4494,7 +5400,10 @@ "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.\nSimpleX servers cannot see your profile." = "Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır.\nSimpleX sunucuları profilinizi göremez."; +"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."; @@ -4502,9 +5411,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Rasgele profiliniz"; -/* No comment provided by engineer. */ -"Your server" = "Sunucunuz"; - /* No comment provided by engineer. */ "Your server address" = "Sunucu adresiniz"; @@ -4514,9 +5420,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "SimpleX adresin"; -/* No comment provided by engineer. */ -"Your SMP servers" = "SMP sunucularınız"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "XFTP sunucularınız"; - diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index d52efd2b65..734b8dda82 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.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,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Зірка на GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Додати контакт**: створити нове посилання-запрошення або підключитися за отриманим посиланням."; - -/* 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 group**: to create a new group." = "**Створити групу**: створити нову групу."; @@ -80,10 +47,10 @@ "**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. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Зверніть увагу**: використання однієї і тієї ж бази даних на двох пристроях порушить розшифровку повідомлень з ваших з'єднань, як захист безпеки."; @@ -92,7 +59,10 @@ "**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 / Paste link**: to connect via a link you received." = "**Відсканувати / Вставити посилання**: підключитися за отриманим посиланням."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку."; @@ -154,6 +124,9 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ перевірено"; +/* No comment provided by engineer. */ +"%@ server" = "%@ сервер"; + /* No comment provided by engineer. */ "%@ servers" = "%@ сервери"; @@ -163,6 +136,9 @@ /* notification title */ "%@ wants to connect!" = "%@ хоче підключитися!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ та %lld учасників"; @@ -175,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 хв"; @@ -229,9 +220,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 секунд"; @@ -277,7 +265,8 @@ /* No comment provided by engineer. */ "0s" = "0с"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 день"; /* time interval */ @@ -286,12 +275,20 @@ /* 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" = "Одноразове посилання"; + +/* 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 хвилин"; @@ -326,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"; @@ -338,57 +332,104 @@ "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 contact" = "Додати контакт"; - -/* 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." = "Адміністратори можуть заблокувати користувача для всіх."; @@ -398,6 +439,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Розширені налаштування мережі"; +/* No comment provided by engineer. */ +"Advanced settings" = "Додаткові налаштування"; + /* chat item text */ "agreeing encryption for %@…" = "узгодження шифрування для %@…"; @@ -413,9 +457,18 @@ /* 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!" = "Усі повідомлення будуть видалені - цю дію не можна скасувати!"; @@ -425,6 +478,9 @@ /* 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." = "Всі ваші контакти залишаться на зв'язку."; @@ -440,9 +496,15 @@ /* 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 downgrade" = "Дозволити пониження версії"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години)"; @@ -458,12 +520,18 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Дозволити надсилання зникаючих повідомлень."; +/* No comment provided by engineer. */ +"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." = "Дозволити надсилати голосові повідомлення."; @@ -500,6 +568,9 @@ /* pref value */ "always" = "завжди"; +/* No comment provided by engineer. */ +"Always use private routing." = "Завжди використовуйте приватну маршрутизацію."; + /* No comment provided by engineer. */ "Always use relay" = "Завжди використовуйте реле"; @@ -512,6 +583,9 @@ /* No comment provided by engineer. */ "Answer call" = "Відповісти на дзвінок"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Кожен може хостити сервери."; + /* No comment provided by engineer. */ "App build: %@" = "Збірка програми: %@"; @@ -530,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" = "Версія програми"; @@ -542,15 +619,27 @@ /* 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" = "Аудіо та відео дзвінки"; @@ -590,9 +679,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" = "Неправильна адреса робочого столу"; @@ -608,12 +703,33 @@ /* 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" = "Блокувати"; @@ -638,12 +754,19 @@ /* rcv group event chat item */ "blocked %@" = "заблоковано %@"; -/* marked deleted chat item preview text */ +/* 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" = "жирний"; @@ -665,9 +788,18 @@ /* 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!" = "Дзвінок вже закінчився!"; @@ -683,9 +815,18 @@ /* No comment provided by engineer. */ "Calls" = "Дзвінки"; +/* No comment provided by engineer. */ +"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!" = "Не вдається запросити контакт!"; @@ -693,6 +834,10 @@ "Can't invite contacts!" = "Неможливо запросити контакти!"; /* No comment provided by engineer. */ +"Can't message member" = "Не можу надіслати повідомлення користувачеві"; + +/* alert action +alert button */ "Cancel" = "Скасувати"; /* No comment provided by engineer. */ @@ -705,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?" = "Змінити пароль до бази даних?"; @@ -735,7 +892,7 @@ "Change self-destruct mode" = "Змінити режим самознищення"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Змінити пароль самознищення"; /* chat item text */ @@ -754,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" = "Консоль чату"; @@ -765,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" = "Імпорт бази даних чату"; @@ -777,16 +946,40 @@ /* 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. */ @@ -802,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. */ @@ -817,10 +1019,13 @@ "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" = "Порівняти файл"; @@ -831,15 +1036,45 @@ /* 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" = "Підтвердьте налаштування мережі"; @@ -873,6 +1108,9 @@ /* 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?" = "З'єднатися з самим собою?"; @@ -897,18 +1135,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)" = "з'єднання (прийнято)"; @@ -930,15 +1177,21 @@ /* 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 error" = "Помилка підключення"; @@ -948,18 +1201,30 @@ /* 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$@"; @@ -969,6 +1234,9 @@ /* 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"; @@ -982,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" = "Ім'я контактної особи"; @@ -990,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" = "Контакти"; @@ -999,12 +1270,21 @@ /* 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 %@?" = "Виправити ім'я на %@?"; @@ -1012,10 +1292,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" = "Створити файл"; @@ -1047,15 +1327,15 @@ /* No comment provided by engineer. */ "Create your profile" = "Створіть свій профіль"; +/* No comment provided by engineer. */ +"Created" = "Створено"; + /* No comment provided by engineer. */ "Created at" = "Створено за адресою"; /* copied message info */ "Created at: %@" = "Створено за адресою: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Створено %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Створення архівного посилання"; @@ -1065,12 +1345,18 @@ /* 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 %@." = "Наразі максимальний підтримуваний розмір файлу - %@."; @@ -1080,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" = "Пониження версії бази даних"; @@ -1143,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. */ @@ -1158,9 +1460,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 повідомлень?"; @@ -1180,10 +1486,7 @@ "Delete and notify contact" = "Видалити та повідомити контакт"; /* No comment provided by engineer. */ -"Delete archive" = "Видалити архів"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Видалити архів чату?"; +"Delete chat" = "Видалити чат"; /* No comment provided by engineer. */ "Delete chat profile" = "Видалити профіль чату"; @@ -1191,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" = "Видалити підключення"; @@ -1198,10 +1504,7 @@ "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" = "Видалити базу даних"; @@ -1245,7 +1548,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Видалити повідомлення?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Видалити повідомлення"; /* No comment provided by engineer. */ @@ -1258,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?" = "Видалити очікуване з'єднання?"; @@ -1269,12 +1572,21 @@ /* 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" = "Видалено за"; @@ -1287,6 +1599,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" = "Доставка"; @@ -1308,9 +1626,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" = "Інструменти для розробників"; @@ -1336,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)" = "Вимкнути (зберегти перевизначення)"; @@ -1350,6 +1689,9 @@ /* No comment provided by engineer. */ "disabled" = "вимкнено"; +/* No comment provided by engineer. */ +"Disabled" = "Вимкнено"; + /* No comment provided by engineer. */ "Disappearing message" = "Зникаюче повідомлення"; @@ -1360,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" = "Зникає за"; @@ -1386,6 +1728,15 @@ /* 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 для екстрених викликів."; @@ -1401,12 +1752,28 @@ /* 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" = "Завантажити архів"; @@ -1419,12 +1786,18 @@ /* 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" = "Редагувати"; @@ -1437,7 +1810,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. */ @@ -1476,6 +1849,12 @@ /* enabled status */ "enabled" = "увімкнено"; +/* No comment provided by engineer. */ +"Enabled" = "Увімкнено"; + +/* No comment provided by engineer. */ +"Enabled for" = "Увімкнено для"; + /* enabled status */ "enabled for contact" = "увімкнено для контакту"; @@ -1596,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" = "Помилка створення адреси"; @@ -1644,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" = "Помилка видалення бази даних"; @@ -1674,26 +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" = "Помилка приєднання до групи"; +/* alert title */ +"Error loading servers" = "Помилка завантаження серверів"; + /* No comment provided by engineer. */ -"Error loading %@ servers" = "Помилка завантаження %@ серверів"; +"Error migrating settings" = "Помилка міграції налаштувань"; /* No comment provided by engineer. */ "Error opening chat" = "Помилка відкриття чату"; -/* No comment provided by engineer. */ +/* 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" = "Помилка збереження профілю групи"; @@ -1707,6 +2107,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Помилка збереження пароля на keychain"; +/* alert title */ +"Error saving servers" = "Сервери збереження помилок"; + /* when migrating */ "Error saving settings" = "Налаштування збереження помилок"; @@ -1735,6 +2138,9 @@ "Error stopping chat" = "Помилка зупинки чату"; /* No comment provided by engineer. */ +"Error switching profile" = "Помилка перемикання профілю"; + +/* alertTitle */ "Error switching profile!" = "Помилка перемикання профілю!"; /* No comment provided by engineer. */ @@ -1746,6 +2152,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Повідомлення про помилку оновлення"; +/* alert title */ +"Error updating server" = "Помилка оновлення сервера"; + /* No comment provided by engineer. */ "Error updating settings" = "Помилка оновлення налаштувань"; @@ -1761,7 +2170,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. */ @@ -1771,10 +2182,13 @@ "Error: URL is invalid" = "Помилка: URL-адреса невірна"; /* 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" = "Вихід без збереження"; @@ -1782,12 +2196,18 @@ /* chat item action */ "Expand" = "Розгорнути"; +/* No comment provided by engineer. */ +"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." = "Експортований архів бази даних."; @@ -1806,9 +2226,27 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Швидше приєднання та надійніші повідомлення."; -/* No comment provided by engineer. */ +/* 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." = "Файл буде видалено з серверів."; @@ -1821,6 +2259,9 @@ /* No comment provided by engineer. */ "File: %@" = "Файл: %@"; +/* No comment provided by engineer. */ +"Files" = "Файли"; + /* No comment provided by engineer. */ "Files & media" = "Файли та медіа"; @@ -1828,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!" = "Файли та медіа заборонені!"; @@ -1866,9 +2310,66 @@ /* 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" = "Знайдено робочий стіл"; @@ -1881,9 +2382,6 @@ /* 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." = "Повністю децентралізована - видима лише для учасників."; @@ -1896,6 +2394,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-файли та наклейки"; +/* message preview */ +"Good afternoon!" = "Доброго дня!"; + +/* message preview */ +"Good morning!" = "Доброго ранку!"; + /* No comment provided by engineer. */ "Group" = "Група"; @@ -1932,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. (24 hours)" = "Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години)"; - -/* 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:" = "Групове повідомлення:"; @@ -2011,7 +2497,10 @@ "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"; @@ -2056,7 +2545,7 @@ "Immediately" = "Негайно"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Імунітет до спаму та зловживань"; +"Immune to spam" = "Імунітет до спаму та зловживань"; /* No comment provided by engineer. */ "Import" = "Імпорт"; @@ -2070,9 +2559,15 @@ /* 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" = "Покращена доставка повідомлень"; @@ -2088,6 +2583,12 @@ /* 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" = "Інкогніто"; @@ -2142,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" = "недійсний чат"; @@ -2181,7 +2685,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Неправильна відповідь"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Неправильна адреса сервера!"; /* item status text */ @@ -2193,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" = "Запросити до групи"; @@ -2220,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" = "Безповоротне видалення повідомлення"; @@ -2227,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." = "Це дозволяє мати багато анонімних з'єднань без будь-яких спільних даних між ними в одному профілі чату."; @@ -2238,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 (%@)." = "Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@)."; @@ -2250,7 +2766,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японський інтерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Приєднуйтесь"; /* No comment provided by engineer. */ @@ -2277,13 +2793,16 @@ /* No comment provided by engineer. */ "Joining group" = "Приєднання до групи"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Тримай"; +/* No comment provided by engineer. */ +"Keep conversation" = "Підтримуйте розмову"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Тримайте додаток відкритим, щоб використовувати його з робочого столу"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Зберігати невикористані запрошення?"; /* No comment provided by engineer. */ @@ -2301,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" = "Покинути групу"; @@ -2340,9 +2865,6 @@ /* No comment provided by engineer. */ "Live messages" = "Живі повідомлення"; -/* No comment provided by engineer. */ -"Local" = "Локально"; - /* No comment provided by engineer. */ "Local name" = "Місцева назва"; @@ -2355,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" = "Позначити видалено для всіх"; @@ -2391,6 +2904,12 @@ /* 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" = "учасник"; @@ -2403,24 +2922,72 @@ /* 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" = "Реакції на повідомлення"; @@ -2428,11 +2995,29 @@ "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" = "Текст повідомлення"; @@ -2448,6 +3033,15 @@ /* 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." = "Повідомлення, файли та дзвінки захищені **наскрізним шифруванням** з ідеальною секретністю переадресації, відмовою та відновленням після злому."; @@ -2488,7 +3082,7 @@ "Migration is completed" = "Міграцію завершено"; /* No comment provided by engineer. */ -"Migrations: %@" = "Міграції: %@"; +"Migrations:" = "Міграції:"; /* time unit */ "minutes" = "хвилини"; @@ -2517,57 +3111,81 @@ /* 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" = "Новий запит на контакт"; /* 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" = "Нова роль учасника"; @@ -2583,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" = "ні"; @@ -2604,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"; @@ -2616,24 +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- відключати користувачів (роль \"спостерігач\")"; @@ -2641,11 +3316,11 @@ "observer" = "спостерігач"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "вимкнено"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Вимкнено"; /* feature offered item */ @@ -2654,7 +3329,7 @@ /* feature offered item */ "offered %@: %@" = "запропонував %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Гаразд"; /* No comment provided by engineer. */ @@ -2663,9 +3338,6 @@ /* No comment provided by engineer. */ "Old database" = "Стара база даних"; -/* No comment provided by engineer. */ -"Old database archive" = "Старий архів бази даних"; - /* group pref value */ "on" = "увімкненo"; @@ -2673,16 +3345,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**." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **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." = "Тільки власники груп можуть змінювати налаштування групи."; @@ -2723,15 +3401,21 @@ /* 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" = "Відкрита група"; @@ -2741,15 +3425,18 @@ /* 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 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" = "Або вставте посилання на архів"; @@ -2762,9 +3449,27 @@ /* 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" = "Пароль"; @@ -2780,6 +3485,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "Пароль встановлено!"; +/* No comment provided by engineer. */ +"Password" = "Пароль"; + /* No comment provided by engineer. */ "Password to show" = "Показати пароль"; @@ -2802,10 +3510,10 @@ "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" = "Постійна помилка розшифрування"; @@ -2819,9 +3527,18 @@ /* 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." = "Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше."; @@ -2864,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" = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; @@ -2871,26 +3591,44 @@ "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" = "З'єднання профілю та сервера"; @@ -2898,15 +3636,15 @@ "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. */ @@ -2930,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" = "Тайм-аут протоколу"; @@ -2946,7 +3693,16 @@ "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-сервер"; @@ -2960,20 +3716,23 @@ /* 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/chat-profiles.html#incognito-mode)." = "Читайте більше в [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"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)."; @@ -2982,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…" = "отримали відповідь…"; @@ -3005,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." = "Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі."; @@ -3017,12 +3785,30 @@ /* 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?" = "Перепідключити сервери?"; @@ -3035,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. */ @@ -3056,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" = "Видалити учасника"; @@ -3107,18 +3900,36 @@ /* 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" = "Перезапустіть програму, щоб створити новий профіль чату"; @@ -3144,7 +3955,7 @@ "Reveal" = "Показувати"; /* No comment provided by engineer. */ -"Revert" = "Повернутися"; +"Review conditions" = "Умови перегляду"; /* No comment provided by engineer. */ "Revoke" = "Відкликати"; @@ -3161,30 +3972,31 @@ /* No comment provided by engineer. */ "Run chat" = "Запустити чат"; +/* No comment provided by engineer. */ +"Safely receive files" = "Безпечне отримання файлів"; + /* No comment provided by engineer. */ "Safer groups" = "Безпечніші групи"; -/* 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. */ "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" = "Зберегти профіль групи"; @@ -3194,7 +4006,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. */ @@ -3203,14 +4015,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?" = "Зберегти вітальне повідомлення?"; + +/* alert title */ +"Save your profile?" = "Зберегти свій профіль?"; /* 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" = "Збережене повідомлення"; @@ -3218,6 +4042,15 @@ /* 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" = "Сканувати код"; @@ -3233,6 +4066,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Відскануйте QR-код сервера"; +/* No comment provided by engineer. */ +"search" = "пошук"; + /* No comment provided by engineer. */ "Search" = "Пошук"; @@ -3245,6 +4081,9 @@ /* network option */ "sec" = "сек"; +/* No comment provided by engineer. */ +"Secondary" = "Вторинний"; + /* time unit */ "seconds" = "секунди"; @@ -3254,6 +4093,9 @@ /* server test step */ "Secure queue" = "Безпечна черга"; +/* No comment provided by engineer. */ +"Secured" = "Забезпечено"; + /* No comment provided by engineer. */ "Security assessment" = "Оцінка безпеки"; @@ -3263,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" = "Самознищення"; @@ -3290,15 +4141,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" = "Надіслати попередній перегляд за посиланням"; @@ -3306,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" = "Надсилайте запитання та ідеї"; @@ -3323,7 +4180,7 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Надішліть до 100 останніх повідомлень новим користувачам."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Відправник скасував передачу файлу."; /* No comment provided by engineer. */ @@ -3359,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" = "Сервер вимагає авторизації для створення черг, перевірте пароль"; @@ -3377,9 +4276,24 @@ /* 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" = "Код сесії"; @@ -3389,6 +4303,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Встановити ім'я контакту…"; +/* No comment provided by engineer. */ +"Set default theme" = "Встановлення теми за замовчуванням"; + /* No comment provided by engineer. */ "Set group preferences" = "Встановіть налаштування групи"; @@ -3419,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 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" = "Показувати дзвінки в історії дзвінків"; @@ -3449,6 +4394,12 @@ /* 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" = "Показати попередній перегляд"; @@ -3458,12 +4409,24 @@ /* 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."; @@ -3479,6 +4442,12 @@ /* 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"; @@ -3494,9 +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" = "Пропустити"; @@ -3507,14 +4482,35 @@ "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. */ +"Square, circle, or anything in between." = "Квадрат, коло або щось середнє між ними."; + /* chat item text */ "standard end-to-end encryption" = "стандартне наскрізне шифрування"; @@ -3527,18 +4523,21 @@ /* 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" = "Припинити чат"; -/* 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." = "Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено."; @@ -3554,10 +4553,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 */ @@ -3569,24 +4568,48 @@ /* 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." = "Натисніть, щоб активувати профіль."; @@ -3606,7 +4629,7 @@ "Tap to scan" = "Натисніть, щоб сканувати"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Натисніть, щоб почати новий чат"; +"TCP connection" = "TCP-з'єднання"; /* No comment provided by engineer. */ "TCP connection timeout" = "Тайм-аут TCP-з'єднання"; @@ -3620,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 %@." = "Тест завершився невдало на кроці %@."; @@ -3629,7 +4655,7 @@ /* No comment provided by engineer. */ "Test servers" = "Тестові сервери"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Тести не пройшли!"; /* No comment provided by engineer. */ @@ -3642,10 +4668,13 @@ "Thanks to the users – contribute via Weblate!" = "Дякуємо користувачам - зробіть свій внесок через Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном."; +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; /* No comment provided by engineer. */ -"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; +"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." = "Спроба змінити пароль до бази даних не була завершена."; @@ -3653,6 +4682,9 @@ /* 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!" = "Прийняте вами з'єднання буде скасовано!"; @@ -3665,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." = "Хеш попереднього повідомлення відрізняється."; @@ -3678,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! ✅" = "Другу галочку ми пропустили! ✅"; @@ -3695,11 +4739,20 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Сервери для нових підключень вашого поточного профілю чату **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "Сервери для нових файлів вашого поточного профілю чату **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Текст, який ви вставили, не є посиланням SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Тема"; +"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 **%@**." = "Ці налаштування стосуються вашого поточного профілю **%@**."; @@ -3743,9 +4796,15 @@ /* 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:" = "Задати будь-які питання та отримувати новини:"; @@ -3759,7 +4818,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." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, 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."; @@ -3767,24 +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: %@)." = "Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@)."; @@ -3821,13 +4913,13 @@ /* rcv group event chat item */ "unblocked %@" = "розблоковано %@"; -/* item status description */ -"Unexpected error: %@" = "Неочікувана помилка: %@"; +/* 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. */ @@ -3854,6 +4946,12 @@ /* 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" = "невідомий статус"; @@ -3875,10 +4973,13 @@ /* 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. */ @@ -3887,9 +4988,6 @@ /* No comment provided by engineer. */ "Update" = "Оновлення"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Оновити налаштування хостів .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Оновити парольну фразу бази даних"; @@ -3897,7 +4995,7 @@ "Update network settings?" = "Оновити налаштування мережі?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Оновити режим транспортної ізоляції?"; +"Update settings?" = "Оновити налаштування?"; /* rcv group event chat item */ "updated group profile" = "оновлений профіль групи"; @@ -3909,10 +5007,10 @@ "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" = "Не вдалося завантфжити"; @@ -3920,18 +5018,33 @@ /* 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" = "Використовуйте для нових з'єднань"; @@ -3947,20 +5060,35 @@ /* 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. */ +"Use SOCKS proxy" = "Використовуйте SOCKS проксі"; + /* No comment provided by engineer. */ "Use the app while in the call." = "Використовуйте додаток під час розмови."; /* No comment provided by engineer. */ -"User profile" = "Профіль користувача"; +"Use the app with one hand." = "Використовуйте додаток однією рукою."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Для використання хостів .onion потрібен сумісний VPN-провайдер."; +"User selection" = "Вибір користувача"; + +/* No comment provided by engineer. */ +"Username" = "Ім'я користувача"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Використання серверів SimpleX Chat."; @@ -4010,6 +5138,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Через безпечний квантово-стійкий протокол."; +/* No comment provided by engineer. */ +"video" = "відео"; + /* No comment provided by engineer. */ "Video call" = "Відеодзвінок"; @@ -4025,9 +5156,15 @@ /* 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" = "Видима історія"; @@ -4041,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!" = "Голосові повідомлення заборонені!"; @@ -4064,6 +5204,12 @@ /* 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!" = "хоче зв'язатися з вами!"; @@ -4095,11 +5241,26 @@ "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." = "З зашифрованими файлами та медіа."; @@ -4109,20 +5270,32 @@ /* 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." = "Ви **не повинні використовувати** одну і ту ж базу даних на двох пристроях."; @@ -4139,6 +5312,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 %@." = "Ви вже з'єднані з %@."; @@ -4169,6 +5345,9 @@ /* 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" = "ви спостерігач"; @@ -4178,6 +5357,12 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми."; +/* No comment provided by engineer. */ +"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" = "Ви можете створити його пізніше"; @@ -4197,7 +5382,13 @@ "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." = "Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань."; @@ -4209,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 можна в Налаштуваннях."; @@ -4220,7 +5411,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Ви можете використовувати розмітку для форматування повідомлень:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Ви можете переглянути посилання на запрошення ще раз у деталях підключення."; /* No comment provided by engineer. */ @@ -4239,10 +5430,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!" = "Ви вже надсилали запит на підключення за цією адресою!"; @@ -4250,9 +5441,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." = "Вам доведеться вводити парольну фразу щоразу під час запуску програми - вона не зберігається на пристрої."; @@ -4268,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." = "Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх."; @@ -4313,6 +5510,9 @@ /* 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." = "Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена."; @@ -4328,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" = "Твої дзвінки"; @@ -4340,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 (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; @@ -4355,6 +5555,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Ваші контакти залишаться на зв'язку."; +/* No comment provided by engineer. */ +"Your credentials may be sent unencrypted." = "Ваші облікові дані можуть бути надіслані незашифрованими."; + /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Ваша поточна база даних чату буде ВИДАЛЕНА і ЗАМІНЕНА імпортованою."; @@ -4377,7 +5580,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." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам.\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." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої."; @@ -4386,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" = "Ваші налаштування"; @@ -4397,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/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 5af77e4e51..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)" = " (可复制)"; @@ -31,23 +16,14 @@ /* 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)"; @@ -59,7 +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 group**: to create a new group." = "**创建群组**: 创建一个新群组."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**端到端加密** 语音通话"; @@ -68,20 +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. */ +"**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 / 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*" = "\\*加粗*"; @@ -124,6 +112,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ 已连接"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ 已下载"; + /* notification title */ "%@ is connected!" = "%@ 已连接!"; @@ -134,11 +125,20 @@ "%@ 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 成员"; @@ -151,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 分钟"; @@ -163,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 星期"; @@ -190,15 +208,21 @@ /* 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 秒"; @@ -238,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 */ @@ -250,12 +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分钟"; @@ -290,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"; @@ -302,104 +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 contact" = "添加联系人"; +"Add friends" = "添加好友"; /* No comment provided by engineer. */ -"Add preset servers" = "添加预设服务器"; +"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. (24 hours)" = "仅有您的联系人许可后才允许不可撤回消息移除。"; +"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." = "只有您的联系人允许时才允许消息回应。"; @@ -414,11 +545,20 @@ "Allow sending disappearing messages." = "允许发送限时消息。"; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages. (24 hours)" = "允许不可撤回地删除已发送消息。"; +"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." = "允许发送语音消息。"; @@ -435,7 +575,7 @@ "Allow your contacts to call you." = "允许您的联系人给您打电话。"; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "允许您的联系人不可撤回地删除已发送消息。"; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "允许您的联系人不可撤回地删除已发送消息"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "允许您的联系人发送限时消息。"; @@ -455,21 +595,39 @@ /* 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" = "应用程序图标"; @@ -479,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" = "应用程序版本"; @@ -488,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" = "语音和视频通话"; @@ -530,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" = "糟糕的桌面地址"; @@ -548,12 +754,39 @@ /* 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" = "封禁"; @@ -578,12 +811,19 @@ /* rcv group event chat item */ "blocked %@" = "已封禁 %@"; -/* marked deleted chat item preview text */ +/* 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" = "加粗"; @@ -591,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. (24 hours)" = "您和您的联系人都可以不可逆转地删除已发送的消息。"; +"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." = "您和您的联系人都可以拨打电话。"; @@ -605,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!" = "通话已结束!"; @@ -623,9 +878,18 @@ /* No comment provided by engineer. */ "Calls" = "通话"; +/* No comment provided by engineer. */ +"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!" = "无法邀请联系人!"; @@ -633,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 %@" = "已取消 %@"; @@ -642,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?" = "更改数据库密码?"; @@ -672,7 +958,7 @@ "Change self-destruct mode" = "更改自毁模式"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "更改自毁密码"; /* chat item text */ @@ -691,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" = "聊天控制台"; @@ -702,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" = "聊天数据库已导入"; @@ -714,18 +1012,48 @@ /* 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" = "选择文件"; @@ -733,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. */ @@ -741,6 +1078,12 @@ /* 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?" = "清除私密笔记?"; @@ -748,10 +1091,16 @@ "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" = "彩色"; + +/* report reason */ +"Community guidelines violation" = "违反社区指导方针"; /* server test step */ "Compare file" = "对比文件"; @@ -762,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…" = "确认新密码……"; @@ -780,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" = "连接"; @@ -795,30 +1189,54 @@ /* 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 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)" = "连接中(已接受)"; @@ -840,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" = "连接错误"; @@ -858,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%@" = "连接被运营方 %@ 阻止"; + +/* 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" = "联系人具有端到端加密"; @@ -889,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" = "联系人姓名"; @@ -897,29 +1351,47 @@ /* 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 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" = "创建文件"; @@ -933,6 +1405,9 @@ /* 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/)中创建新的个人资料。 💻"; @@ -952,10 +1427,16 @@ "Create your profile" = "创建您的资料"; /* No comment provided by engineer. */ -"Created at" = "创建于"; +"Created" = "已创建"; /* 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…" = "创建链接中…"; @@ -963,12 +1444,18 @@ /* 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 %@." = "目前支持的最大文件大小为 %@。"; @@ -978,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" = "数据库降级"; @@ -1041,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. */ @@ -1056,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" = "删除地址"; @@ -1075,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" = "删除聊天资料"; @@ -1086,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" = "删除连接"; @@ -1093,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" = "删除文件"; @@ -1128,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. */ @@ -1147,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?" = "删除待定连接?"; @@ -1158,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" = "已删除于"; @@ -1176,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." = "已送达,即使苹果已将其删除。"; + /* No comment provided by engineer. */ "Delivery" = "传送"; @@ -1191,12 +1728,33 @@ /* 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" = "开发者工具"; @@ -1204,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 锁定。"; @@ -1222,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" = "全部禁用"; @@ -1236,6 +1803,9 @@ /* No comment provided by engineer. */ "disabled" = "关闭"; +/* No comment provided by engineer. */ +"Disabled" = "禁用"; + /* No comment provided by engineer. */ "Disappearing message" = "限时消息"; @@ -1246,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" = "消失于"; @@ -1272,36 +1842,85 @@ /* 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" = "编辑"; @@ -1314,15 +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?" = "启用即时通知?"; @@ -1350,6 +1975,12 @@ /* enabled status */ "enabled" = "已启用"; +/* No comment provided by engineer. */ +"Enabled" = "已启用"; + +/* No comment provided by engineer. */ +"Enabled for" = "启用对象"; + /* enabled status */ "enabled for contact" = "已为联系人启用"; @@ -1374,6 +2005,9 @@ /* notification */ "Encrypted message or another event" = "加密消息或其他事件"; +/* notification */ +"Encrypted message: app is stopped" = "加密消息:应用程序已停止"; + /* notification */ "Encrypted message: database error" = "加密消息:数据库错误"; @@ -1419,6 +2053,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "需要为 %@ 重新进行加密协商"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "正进行加密重协商。"; + /* No comment provided by engineer. */ "ended" = "已结束"; @@ -1428,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…" = "输入密码……"; @@ -1449,6 +2092,9 @@ /* placeholder */ "Enter welcome message… (optional)" = "输入欢迎消息……(可选)"; +/* No comment provided by engineer. */ +"Enter your name…" = "请输入您的姓名…"; + /* No comment provided by engineer. */ "error" = "错误"; @@ -1458,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" = "创建地址错误"; @@ -1485,6 +2143,9 @@ /* No comment provided by engineer. */ "Error creating group link" = "创建群组链接错误"; +/* alert title */ +"Error creating list" = "创建列表出错"; + /* No comment provided by engineer. */ "Error creating member contact" = "创建成员联系人时出错"; @@ -1494,6 +2155,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "创建资料错误!"; +/* No comment provided by engineer. */ +"Error creating report" = "创建举报出错"; + /* No comment provided by engineer. */ "Error decrypting file" = "解密文件时出错"; @@ -1506,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" = "删除数据库错误"; @@ -1521,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!" = "启用送达回执出错!"; @@ -1533,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" = "保存群组资料错误"; @@ -1563,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" = "发送电邮错误"; @@ -1585,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" = "更新设置错误"; @@ -1603,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. */ @@ -1615,10 +2329,13 @@ "Error: URL is invalid" = "错误:URL 无效"; /* 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" = "退出而不保存"; @@ -1626,15 +2343,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…" = "导出数据库档案中…"; @@ -1644,12 +2373,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." = "文件将从服务器中删除。"; @@ -1662,6 +2421,9 @@ /* No comment provided by engineer. */ "File: %@" = "文件:%@"; +/* No comment provided by engineer. */ +"Files" = "文件"; + /* No comment provided by engineer. */ "Files & media" = "文件和媒体"; @@ -1669,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!" = "禁止文件和媒体!"; @@ -1677,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! 🚀" = "终于我们有它们了! 🚀"; @@ -1701,9 +2472,72 @@ /* 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" = "找到了桌面"; @@ -1716,9 +2550,6 @@ /* 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." = "完全去中心化 - 仅对成员可见。"; @@ -1728,12 +2559,24 @@ /* 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!" = "群已存在!"; @@ -1764,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. (24 hours)" = "群组成员可以不可撤回地删除已发送的消息。"; - -/* 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:" = "群组消息:"; @@ -1809,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" = "隐藏"; @@ -1843,6 +2674,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. */ @@ -1857,6 +2694,9 @@ /* 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 服务器(每行一个)"; @@ -1885,7 +2725,7 @@ "Immediately" = "立即"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "不受垃圾和骚扰消息影响"; +"Immune to spam" = "不受垃圾和骚扰消息影响"; /* No comment provided by engineer. */ "Import" = "导入"; @@ -1896,6 +2736,18 @@ /* 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" = "改进了消息传递"; @@ -1905,9 +2757,24 @@ /* 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" = "隐身聊天"; @@ -1962,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" = "无效聊天"; @@ -1986,6 +2871,12 @@ /* 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!" = "无效名称!"; @@ -1993,6 +2884,9 @@ "Invalid QR code" = "无效的二维码"; /* No comment provided by engineer. */ +"Invalid response" = "无效的响应"; + +/* alert title */ "Invalid server address!" = "无效的服务器地址!"; /* item status text */ @@ -2004,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" = "邀请加入群组"; @@ -2031,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" = "不可撤回消息移除"; @@ -2038,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." = "它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。"; @@ -2049,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 (%@)." = "您似乎已经通过此链接连接。如果不是这样,则有一个错误 (%@)。"; @@ -2061,7 +2967,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "日语界面"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "加入"; /* No comment provided by engineer. */ @@ -2080,12 +2986,24 @@ "Join incognito" = "加入隐身聊天"; /* No comment provided by engineer. */ -"Joining group" = "加入群组中"; +"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. */ @@ -2103,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" = "离开群组"; @@ -2133,6 +3057,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" = "实时"; @@ -2142,9 +3075,6 @@ /* No comment provided by engineer. */ "Live messages" = "实时消息"; -/* No comment provided by engineer. */ -"Local" = "本地"; - /* No comment provided by engineer. */ "Local name" = "本地名称"; @@ -2157,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" = "标记为所有人已删除"; @@ -2193,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" = "消息回应"; @@ -2227,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:" = "迁移错误:"; @@ -2254,7 +3304,7 @@ "Migration is completed" = "迁移完成"; /* No comment provided by engineer. */ -"Migrations: %@" = "迁移:%@"; +"Migrations:" = "迁移"; /* time unit */ "minutes" = "分钟"; @@ -2280,60 +3330,93 @@ /* 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" = "新成员角色"; @@ -2349,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" = "否"; @@ -2358,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" = "未选择联系人"; @@ -2370,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" = "无端到端加密"; @@ -2382,24 +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- 禁用成员(“观察员”角色)"; @@ -2407,11 +3574,11 @@ "observer" = "观察者"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "关闭"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "关闭"; /* feature offered item */ @@ -2420,7 +3587,7 @@ /* feature offered item */ "offered %@: %@" = "已提供 %1$@:%2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "好的"; /* No comment provided by engineer. */ @@ -2429,9 +3596,6 @@ /* No comment provided by engineer. */ "Old database" = "旧的数据库"; -/* No comment provided by engineer. */ -"Old database archive" = "旧数据库存档"; - /* group pref value */ "on" = "开启"; @@ -2439,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." = "只有群主可以改变群组偏好设置。"; @@ -2459,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). (24 hours)" = "只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。"; +"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." = "只有您可以拨打电话。"; @@ -2478,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). (24 hours)" = "只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。"; +"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." = "只有您的联系人可以拨打电话。"; @@ -2489,36 +3665,75 @@ /* 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. */ +"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" = "密码"; @@ -2534,9 +3749,15 @@ /* No comment provided by engineer. */ "Passcode set!" = "密码已设置!"; +/* No comment provided by engineer. */ +"Password" = "密码"; + /* No comment provided by engineer. */ "Password to show" = "显示密码"; +/* past/unknown group member */ +"Past member %@" = "前任成员 %@"; + /* No comment provided by engineer. */ "Paste desktop address" = "粘贴桌面地址"; @@ -2553,23 +3774,35 @@ "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." = "请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。"; @@ -2579,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." = "请联系群组管理员。"; @@ -2612,27 +3851,45 @@ /* 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" = "资料和服务器连接"; @@ -2640,12 +3897,15 @@ "Profile image" = "资料图片"; /* 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. */ @@ -2669,41 +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/chat-profiles.html#incognito-mode)." = "阅读更多[User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)。"; +"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) 中阅读更多内容。"; @@ -2712,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…" = "已收到回复……"; @@ -2735,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." = "接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。"; @@ -2744,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?" = "是否重新连接服务器?"; @@ -2762,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. */ @@ -2783,6 +4101,9 @@ /* No comment provided by engineer. */ "Remove" = "移除"; +/* No comment provided by engineer. */ +"Remove image" = "移除图片"; + /* No comment provided by engineer. */ "Remove member" = "删除成员"; @@ -2819,9 +4140,18 @@ /* 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" = "回复"; @@ -2831,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" = "重新启动应用程序以创建新的聊天资料"; @@ -2862,54 +4207,61 @@ "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. */ @@ -2918,14 +4270,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?" = "保存欢迎信息?"; + +/* alert title */ +"Save your profile?" = "保存您的个人资料?"; /* 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" = "已保存的消息"; @@ -2933,6 +4297,15 @@ /* 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" = "扫码"; @@ -2948,6 +4321,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "扫描服务器二维码"; +/* No comment provided by engineer. */ +"search" = "搜索"; + /* No comment provided by engineer. */ "Search" = "搜索"; @@ -2960,6 +4336,9 @@ /* network option */ "sec" = "秒"; +/* No comment provided by engineer. */ +"Secondary" = "二级"; + /* time unit */ "seconds" = "秒"; @@ -2969,6 +4348,9 @@ /* server test step */ "Secure queue" = "保护队列"; +/* No comment provided by engineer. */ +"Secured" = "担保"; + /* No comment provided by engineer. */ "Security assessment" = "安全评估"; @@ -2978,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" = "自毁"; @@ -3005,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" = "发送链接预览"; @@ -3021,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" = "发送问题和想法"; @@ -3038,7 +4432,7 @@ /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "给新成员发送最多 100 条历史消息。"; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "发送人已取消文件传输。"; /* No comment provided by engineer. */ @@ -3074,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" = "服务器需要授权才能创建队列,检查密码"; @@ -3092,9 +4513,24 @@ /* 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" = "会话码"; @@ -3104,6 +4540,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "设置联系人姓名……"; +/* No comment provided by engineer. */ +"Set default theme" = "设置默认主题"; + /* No comment provided by engineer. */ "Set group preferences" = "设置群组偏好设置"; @@ -3119,6 +4558,9 @@ /* No comment provided by engineer. */ "Set passcode" = "设置密码"; +/* No comment provided by engineer. */ +"Set passphrase" = "设置密码短语"; + /* No comment provided by engineer. */ "Set passphrase to export" = "设置密码来导出"; @@ -3131,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. */ @@ -3140,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 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" = "在电话历史记录中显示通话"; @@ -3161,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 审核。"; @@ -3180,14 +4659,20 @@ "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 are prohibited." = "此群禁止 SimpleX 链接。"; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "不允许SimpleX 链接"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX 锁定"; @@ -3203,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" = "跳过"; @@ -3216,14 +4707,32 @@ "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. */ +"Square, circle, or anything in between." = "方形、圆形、或两者之间的任意形状."; + +/* chat item text */ +"standard end-to-end encryption" = "标准端到端加密"; + /* No comment provided by engineer. */ "Start chat" = "开始聊天"; @@ -3233,14 +4742,20 @@ /* 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." = "停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,您将无法收发消息。"; @@ -3257,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"; @@ -3306,7 +4836,7 @@ "Tap to scan" = "轻按扫描"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "点击开始一个新聊天"; +"TCP connection" = "TCP 连接"; /* No comment provided by engineer. */ "TCP connection timeout" = "TCP 连接超时"; @@ -3320,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 %@." = "在步骤 %@ 上测试失败。"; @@ -3329,7 +4862,7 @@ /* No comment provided by engineer. */ "Test servers" = "测试服务器"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "测试失败!"; /* No comment provided by engineer. */ @@ -3342,10 +4875,10 @@ "Thanks to the users – contribute via Weblate!" = "感谢用户——通过 Weblate 做出贡献!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "第一个没有任何用户标识符的平台 - 隐私设计."; +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。"; /* No comment provided by engineer. */ -"The app can notify you when you receive messages or contact requests - please open settings to enable." = "该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。"; +"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." = "更改数据库密码的尝试未完成。"; @@ -3365,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." = "上一条消息的散列不同。"; @@ -3378,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! ✅" = "我们错过的第二个\"√\"!✅"; @@ -3399,7 +4938,7 @@ "The text you pasted is not a SimpleX link." = "您粘贴的文本不是 SimpleX 链接。"; /* No comment provided by engineer. */ -"Theme" = "主题"; +"Themes" = "主题"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "这些设置适用于您当前的配置文件 **%@**。"; @@ -3416,6 +4955,12 @@ /* 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" = "这个联系人"; @@ -3437,9 +4982,15 @@ /* 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:" = "要提出任何问题并接收更新,请:"; @@ -3452,15 +5003,18 @@ /* 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 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." = "请授权使用麦克风以录制语音消息。"; @@ -3473,18 +5027,33 @@ /* 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: %@)." = "正在尝试连接到用于从该联系人接收消息的服务器(错误:%@)。"; /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact." = "正在尝试连接到用于从该联系人接收消息的服务器。"; +/* No comment provided by engineer. */ +"Turkish interface" = "土耳其语界面"; + /* No comment provided by engineer. */ "Turn off" = "关闭"; @@ -3509,13 +5078,13 @@ /* No comment provided by engineer. */ "Unblock member?" = "解封成员吗?"; -/* item status description */ -"Unexpected error: %@" = "意外错误: %@"; +/* 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. */ @@ -3542,6 +5111,12 @@ /* 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" = "未知状态"; @@ -3563,10 +5138,13 @@ /* 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. */ @@ -3575,9 +5153,6 @@ /* No comment provided by engineer. */ "Update" = "更新"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "更新 .onion 主机设置?"; - /* No comment provided by engineer. */ "Update database passphrase" = "更新数据库密码"; @@ -3585,7 +5160,7 @@ "Update network settings?" = "更新网络设置?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "更新传输隔离模式?"; +"Update settings?" = "更新设置?"; /* rcv group event chat item */ "updated group profile" = "已更新的群组资料"; @@ -3597,14 +5172,26 @@ "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 主机"; @@ -3626,6 +5213,15 @@ /* 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" = "使用服务器"; @@ -3633,14 +5229,20 @@ "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%@ (%@)"; @@ -3656,6 +5258,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" = "验证安全码"; @@ -3677,6 +5285,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "通过安全的、抗量子计算机破解的协议。"; +/* No comment provided by engineer. */ +"video" = "视频"; + /* No comment provided by engineer. */ "Video call" = "视频通话"; @@ -3708,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!" = "语音消息禁止发送!"; @@ -3719,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" = "等待文件中"; @@ -3728,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!" = "警告:您可能会丢失部分数据!"; @@ -3746,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" = "更新内容"; @@ -3753,11 +5382,23 @@ "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." = "加密的文件和媒体。"; @@ -3767,20 +5408,35 @@ /* 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" = "您已接受连接"; @@ -3794,12 +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." = "您已连接到用于接收该联系人消息的服务器。"; @@ -3809,12 +5480,21 @@ /* 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 change it in Appearance settings." = "您可以在外观设置中更改它。"; + /* No comment provided by engineer. */ "You can create it later" = "您可以以后创建它"; @@ -3824,6 +5504,9 @@ /* 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." = "您可以隐藏或静音用户个人资料——只需向右滑动。"; @@ -3831,7 +5514,10 @@ "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." = "您可以通过设置来设置锁屏通知预览。"; @@ -3843,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 锁定。"; @@ -3854,7 +5540,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "您可以使用 markdown 来编排消息格式:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "您可以在连接详情中再次查看邀请链接。"; /* No comment provided by engineer. */ @@ -3873,16 +5559,16 @@ "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!" = "你已经请求通过此地址进行连接!"; /* No comment provided by engineer. */ -"You have no chats" = "您没有聊天记录"; +"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." = "您必须在每次应用程序启动时输入密码——它不存储在设备上。"; @@ -3899,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." = "您需要允许您的联系人发送语音消息,以便您能够发送语音消息。"; @@ -3920,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!" = "当您的连接请求被接受后,您将可以连接,请稍等或稍后检查!"; @@ -3953,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" = "您的通话"; @@ -3968,9 +5666,6 @@ /* 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 (%@)." = "您的联系人发送的文件大于当前支持的最大大小 (%@)。"; @@ -3995,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." = "您的资料、联系人和发送的消息存储在您的设备上。"; @@ -4007,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" = "您的服务器地址"; @@ -4019,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 4e279d3ccb..8fc9d104d2 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -15,7 +15,7 @@ android { namespace = "chat.simplex.app" minSdk = 26 //noinspection OldTargetApi - targetSdk = 33 + targetSdk = 34 // !!! // skip version code after release to F-Droid, as it uses two version codes versionCode = (extra["android.version_code"] as String).toInt() @@ -85,9 +85,11 @@ android { "en", "ar", "bg", + "ca", "cs", "de", "es", + "fa", "fi", "fr", "hu", @@ -125,29 +127,29 @@ android { dependencies { implementation(project(":common")) - implementation("androidx.core:core-ktx:1.12.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.6.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") - implementation("androidx.lifecycle:lifecycle-process:2.7.0") - implementation("androidx.activity:activity-compose:1.8.2") - val workVersion = "2.9.0" + 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.2.0") + implementation("com.jakewharton:process-phoenix:3.0.0") //Camera Permission - implementation("com.google.accompanist:accompanist-permissions:0.23.0") + 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.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + 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.6.4") } diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index 073f1bf8c8..0470977bcd 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -21,6 +21,20 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -109,7 +148,6 @@ android:launchMode="singleInstance" android:supportsPictureInPicture="true" android:autoRemoveFromRecents="true" - android:screenOrientation="portrait" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/> + android:stopWithTask="false" + android:foregroundServiceType="remoteMessaging" + /> @@ -141,7 +181,9 @@ android:name=".CallService" android:enabled="true" android:exported="false" - android:stopWithTask="false"/> + android:stopWithTask="false" + android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging" + /> + + + + + + + + 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 index f0ddf8f7de..6c3d96bebc 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/CallService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/CallService.kt @@ -2,17 +2,19 @@ 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.call.CallState import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.datetime.Instant @@ -34,7 +36,7 @@ class CallService: Service() { } else { Log.d(TAG, "null intent. Probably restarted by the system.") } - startForeground(CALL_SERVICE_ID, serviceNotification) + ServiceCompat.startForeground(this, CALL_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType()) return START_STICKY } @@ -42,8 +44,7 @@ class CallService: Service() { super.onCreate() Log.d(TAG, "Call service created") notificationManager = createNotificationChannel() - updateNotification() - startForeground(CALL_SERVICE_ID, serviceNotification) + ServiceCompat.startForeground(this, CALL_SERVICE_ID, updateNotification(), foregroundServiceType()) } override fun onDestroy() { @@ -69,22 +70,52 @@ class CallService: Service() { } } - fun updateNotification() { + 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?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call) + 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() - serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt) - startForeground(CALL_SERVICE_ID, serviceNotification) + 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? { 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 6ce582cad4..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 @@ -4,8 +4,10 @@ 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.activity.enableEdgeToEdge import androidx.compose.ui.platform.ClipboardManager import androidx.fragment.app.FragmentActivity import chat.simplex.app.model.NtfManager @@ -18,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) { @@ -43,6 +56,7 @@ class MainActivity: FragmentActivity() { WindowManager.LayoutParams.FLAG_SECURE ) } + enableEdgeToEdge() setContent { AppScreen() } @@ -50,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) @@ -146,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)) } } } @@ -213,5 +229,5 @@ fun isMediaIntent(intent: Intent): Boolean = // val str: String = """ // """.trimIndent() // -// println(json.decodeFromString(str)) +// println(json.decodeFromString(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 0152f5e8c2..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 @@ -5,9 +5,8 @@ import android.util.Log import androidx.work.* 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.common.platform.chatModel -import chat.simplex.common.platform.initChatControllerAndRunMigrations import chat.simplex.common.views.helpers.DatabaseUtils import kotlinx.coroutines.* import java.util.Date @@ -30,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) } } @@ -60,7 +61,7 @@ class MessagesFetcherWork( 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) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } withTimeout(durationSeconds * 1000L) { val chatController = 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 83105c678a..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 @@ -6,29 +6,32 @@ import android.content.Context import chat.simplex.common.platform.Log import android.content.Intent import android.content.pm.ActivityInfo -import android.media.AudioManager 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.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.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 @@ -64,17 +67,20 @@ class SimplexApp: Application(), LifecycleEventObserver { } } context = this - initHaskell() + initHaskell(packageName) initMultiplatform() + reconfigureBroadcastReceivers() + runMigrations() tmpDir.deleteRecursively() tmpDir.mkdir() + deleteOldChatArchive() // 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) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp) } @@ -86,7 +92,7 @@ class SimplexApp: Application(), LifecycleEventObserver { 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())) @@ -99,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()) } } @@ -117,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() } @@ -142,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() @@ -158,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() @@ -179,6 +190,7 @@ class SimplexApp: Application(), LifecycleEventObserver { 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 Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first }) override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() override fun cancelCallNotification() = NtfManager.cancelCallNotification() @@ -203,6 +215,7 @@ class SimplexApp: Application(), LifecycleEventObserver { appPrefs.backgroundServiceNoticeShown.set(false) } SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) + SimplexService.AppUpdateReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) CoroutineScope(Dispatchers.Default).launch { if (mode == NotificationsMode.SERVICE) { SimplexService.start() @@ -217,7 +230,9 @@ class SimplexApp: Application(), LifecycleEventObserver { SimplexService.safeStopService() } } - + if (mode != NotificationsMode.SERVICE) { + getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) + } if (mode != NotificationsMode.PERIODIC) { MessagesFetcherWorker.cancelAll() } @@ -234,6 +249,7 @@ class SimplexApp: Application(), LifecycleEventObserver { } override fun androidChatStopped() { + getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) SimplexService.safeStopService() MessagesFetcherWorker.cancelAll() } @@ -253,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 @@ -268,6 +283,35 @@ 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) @@ -294,6 +338,8 @@ class SimplexApp: Application(), LifecycleEventObserver { NetworkObserver.shared.restartNetworkObserver() } + override fun androidIsXiaomiDevice(): Boolean = setOf("xiaomi", "redmi", "poco").contains(Build.BRAND.lowercase()) + @SuppressLint("SourceLockedOrientationActivity") @Composable override fun androidLockPortraitOrientation() { @@ -319,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 f56bf4fe00..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,19 +4,24 @@ 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 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 @@ -52,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. @@ -77,7 +79,7 @@ class SimplexService: Service() { 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) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } } } @@ -103,6 +105,26 @@ 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 || isCheckingNewMessages) return @@ -117,6 +139,7 @@ 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@withLongRunningApi } @@ -226,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 @@ -272,7 +318,7 @@ class SimplexService: Service() { 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! } @@ -292,6 +338,10 @@ class SimplexService: Service() { } 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 { @@ -438,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 { @@ -522,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)) ) } @@ -549,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) { @@ -593,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) } @@ -644,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 f83933b2e0..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 @@ -48,11 +48,12 @@ object NtfManager { } private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private var prevNtfTime = mutableMapOf() + // (UserId, ChatId) -> Time + private var prevNtfTime = mutableMapOf, Long>() private val msgNtfTimeoutMs = 30000L init { - if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert() + if (areNotificationsEnabledInSystem()) createNtfChannelsMaybeShowAlert() } private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel { @@ -72,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) } @@ -87,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 @@ -271,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 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 index b78f3ac518..995b584fce 100644 --- 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 @@ -24,8 +24,7 @@ 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.platform.LocalView +import androidx.compose.ui.platform.* import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -37,7 +36,9 @@ 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.* @@ -49,6 +50,7 @@ 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 { @@ -56,6 +58,7 @@ 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) { @@ -80,6 +83,7 @@ class CallActivity: ComponentActivity(), ServiceConnection { override fun onDestroy() { super.onDestroy() + (mainActivity.get() ?: this).applyAppLocale(appPrefs.appLanguage) if (isOnLockScreenNow()) { lockAfterIncomingCall() } @@ -95,7 +99,8 @@ class CallActivity: ComponentActivity(), ServiceConnection { 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() - .setAspectRatio(viewRatio) + // 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) @@ -116,14 +121,15 @@ class CallActivity: ComponentActivity(), ServiceConnection { private fun hasGrantedPermissions(): Boolean { val grantedAudio = ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED - val grantedCamera = !callSupportsVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == 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() && !callSupportsVideo()) { + } else if (!hasGrantedPermissions() && !callHasVideo()) { val call = m.activeCall.value if (call != null) { withBGApi { chatModel.callManager.endCall(call) } @@ -139,8 +145,9 @@ class CallActivity: ComponentActivity(), ServiceConnection { } 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 && callSupportsVideo() && platform.androidPictureInPictureAllowed()) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callHasVideo() && platform.androidPictureInPictureAllowed()) { enterPictureInPictureMode() } } @@ -196,7 +203,7 @@ class CallActivity: ComponentActivity(), ServiceConnection { fun getKeyguardManager(context: Context): KeyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager -private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video +private fun callHasVideo() = m.activeCall.value?.hasVideo == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video @Composable fun CallActivityView() { @@ -210,7 +217,7 @@ fun CallActivityView() { .collect { collapsed -> when { collapsed -> { - if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) { + if (!platform.androidPictureInPictureAllowed() || !callHasVideo()) { activity.moveTaskToBack(true) activity.startActivity(Intent(activity, MainActivity::class.java)) } else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) { @@ -219,7 +226,7 @@ fun CallActivityView() { activity.enterPictureInPictureMode() } } - callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> { + callHasVideo() && !platform.androidPictureInPictureAllowed() -> { // PiP disabled by user platform.androidStartCallActivity(false) } @@ -231,7 +238,7 @@ fun CallActivityView() { } SimpleXTheme { var prevCall by remember { mutableStateOf(call) } - KeyChangeEffect(m.activeCall.value) { + KeyChangeEffect(m.activeCall.value, remember { appPrefs.appLanguage.state }.value) { if (m.activeCall.value != null) { prevCall = m.activeCall.value activity.boundService?.updateNotification() @@ -240,25 +247,43 @@ fun CallActivityView() { Box(Modifier.background(Color.Black)) { if (call != null) { val permissionsState = rememberMultiplePermissionsState( - permissions = if (callSupportsVideo()) { + permissions = if (callHasVideo()) { listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) } else { listOf(Manifest.permission.RECORD_AUDIO) } ) - if (permissionsState.allPermissionsGranted) { + // 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() - } else { - CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) { + 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 (callSupportsVideo()) { + if (callHasVideo()) { val scope = rememberCoroutineScope() LaunchedEffect(Unit) { scope.launch { - activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height)) + activity.setPipParams(callHasVideo(), viewRatio = Rational(view.width, view.height)) activity.trackPipAnimationHintView(view) } } @@ -285,11 +310,6 @@ fun CallActivityView() { AlertManager.shared.showInView() } } - LaunchedEffect(call == null) { - if (call != null) { - activity.startServiceAndBind() - } - } LaunchedEffect(invitation, call, switchingCall, showCallView) { if (!switchingCall && invitation == null && (!showCallView || call == null)) { Log.d(TAG, "CallActivityView: finishing activity") @@ -424,6 +444,7 @@ fun PreviewIncomingCallLockScreenAlert() { ) { IncomingCallLockScreenAlertLayout( invitation = RcvCallInvitation( + callUUID = "", remoteHostId = null, user = User.sampleData, contact = Contact.sampleData, 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 @@ #FF000000 #FFFFFFFF - #8b8786 #121212 \ No newline at end of file diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 1f55a7195c..e2927e4aaf 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -37,7 +37,7 @@ kotlin { 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.58.0") + 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 @@ -48,20 +48,26 @@ kotlin { // Resources 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.8.2") - val workVersion = "2.9.0" + implementation("androidx.activity:activity-compose:1.9.1") + val workVersion = "2.9.1" implementation("androidx.work:work-runtime-ktx:$workVersion") - implementation("com.google.accompanist:accompanist-insets:0.30.1") // Video support implementation("com.google.android.exoplayer:exoplayer:2.19.1") @@ -75,33 +81,42 @@ kotlin { //Camera Permission implementation("com.google.accompanist:accompanist-permissions:0.34.0") - implementation("androidx.webkit:webkit:1.10.0") + implementation("androidx.webkit:webkit:1.11.0") // GIFs support implementation("io.coil-kt:coil-compose:2.6.0") implementation("io.coil-kt:coil-gif:2.6.0") - implementation("com.jakewharton:process-phoenix:2.2.0") + // Emojis + implementation("androidx.emoji2:emoji2-emojipicker:1.4.0") - val cameraXVersion = "1.3.2" + 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.4.1") + implementation("androidx.lifecycle:lifecycle-process:2.8.4") } } val desktopMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0") - implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") + 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.12") - implementation("uk.co.caprica:vlcj:4.8.2") + 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 @@ -115,8 +130,8 @@ android { defaultConfig { minSdk = 26 } - testOptions.targetSdk = 33 - lint.targetSdk = 33 + testOptions.targetSdk = 34 + lint.targetSdk = 34 val isAndroid = gradle.startParameter.taskNames.find { val lower = it.lowercase() lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install") @@ -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"]}") } } @@ -254,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/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/platform/AppCommon.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt index 547db51bad..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 @@ -6,8 +6,8 @@ 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.* @@ -24,27 +24,26 @@ var isAppOnForeground: Boolean = false @Suppress("ConstantLocale") val defaultLocale: Locale = Locale.getDefault() +actual fun isAppVisibleAndFocused(): Boolean = isAppOnForeground + @SuppressLint("StaticFieldLeak") lateinit var androidAppContext: Context var mainActivity: WeakReference = WeakReference(null) var callActivity: WeakReference = 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() @@ -58,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") @@ -68,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 { 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/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 b103367fe8..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,28 +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) -> 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, sendMsgEnabled: Boolean, + disabledText: String?, sendMsgButtonDisabled: Boolean, textStyle: MutableState, showDeleteTextButton: MutableState, - userIsObserver: Boolean, - onMessageChange: (String) -> Unit, + placeholder: String, + showVoiceButton: Boolean, + onMessageChange: (ComposeMessage) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List) -> 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?, 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, padding: PaddingValues) { +private fun ComposeOverlay(text: String, textStyle: MutableState, 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 4dbc9bd9a9..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 Unit>?> = mutableStateOf(null) + override val currentlyPlaying: MutableState = 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 { @@ -168,7 +163,7 @@ actual object AudioPlayer: AudioPlayerInterface { } 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, duration: MutableState, 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, filePath: String?) { pro.value = ms - if (currentlyPlaying.value?.first == filePath) { + if (currentlyPlaying.value?.fileSource?.filePath == filePath) { player.seekTo(ms) } } 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 index 199e719703..0d07de28f8 100644 --- 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 @@ -4,23 +4,105 @@ import androidx.compose.foundation.* import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* -import androidx.compose.runtime.Composable +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, + state: LazyListState?, contentPadding: PaddingValues, reverseLayout: Boolean, verticalArrangement: Arrangement.Vertical, horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, + additionalBarOffset: State?, + additionalTopBar: State, + chatBottomBar: State, + fillMaxSize: Boolean, content: LazyListScope.() -> Unit ) { - LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + 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?, + additionalTopBar: State, + chatBottomBar: State, + maxHeight: State?, + containerAlignment: Alignment, + content: LazyListScope.() -> Unit + ) { + val state = state ?: rememberLazyListState() + LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) { + content() + } } @Composable @@ -28,8 +110,82 @@ actual fun ColumnWithScrollBar( modifier: Modifier, verticalArrangement: Arrangement.Vertical, horizontalAlignment: Alignment.Horizontal, - state: ScrollState, - content: @Composable ColumnScope.() -> Unit + state: ScrollState?, + maxIntrinsicSize: Boolean, + fillMaxSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) ) { - Column(modifier.verticalScroll(rememberScrollState()), verticalArrangement, horizontalAlignment, content) + 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 385e6c82a4..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,19 +3,30 @@ 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) { var text = text for (i in 10 downTo 1) { @@ -37,7 +48,7 @@ actual fun ClipboardManager.shareText(text: String) { } } -fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) { +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() @@ -67,9 +78,35 @@ fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) { 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 = 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) { 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 c90946c95b..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,20 +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.clear -import chat.simplex.common.ui.theme.SimpleXTheme +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() @@ -43,28 +45,13 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current @Composable actual fun getKeyboardState(): State { - 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?, clearFocus: Boolean) { @@ -87,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 } 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 index bada85746f..0c23e95285 100644 --- 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 @@ -47,7 +47,7 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface { Log.d(TAG, "Added audio devices2: ${devices.value.map { it.type }}") if (devices.value.size - oldDevices.size > 0) { - selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false) + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false) } } @@ -71,8 +71,12 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface { } override fun stop() { - am.unregisterAudioDeviceCallback(audioCallback) - am.removeOnCommunicationDeviceChangedListener(listener) + try { + am.unregisterAudioDeviceCallback(audioCallback) + am.removeOnCommunicationDeviceChangedListener(listener) + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } } override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) { @@ -116,14 +120,14 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface { 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?.supportsVideo() == true, false) + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false) } override fun onAudioDevicesRemoved(removedDevices: Array) { 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?.supportsVideo() == true, true) + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, true) } } 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 c702264d82..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 @@ -6,37 +6,43 @@ 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.semantics.Role import androidx.compose.ui.text.TextStyle 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 androidx.compose.ui.viewinterop.AndroidView +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.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -50,6 +56,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString +import java.io.Closeable // Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak @SuppressLint("StaticFieldLeak") @@ -64,49 +71,62 @@ fun activeCallDestroyWebView() = withApi { Log.d(TAG, "CallView: webview was destroyed") } -@SuppressLint("SourceLockedOrientationActivity") -@Composable -actual fun ActiveCallView() { - val call = remember { chatModel.activeCall }.value - val scope = rememberCoroutineScope() - val proximityLock = remember { +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() + } + } + + 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) - 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 } } - val wasConnected = rememberSaveable { mutableStateOf(false) } +} + + +@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 && !wasConnected.value) { + if (call?.callState == CallState.Connected && callState != null && !callState.wasConnected) { CallSoundsPlayer.vibrate(2) - wasConnected.value = true + callState.wasConnected = true } } - val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() } - DisposableEffect(Unit) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - callAudioDeviceManager.start() - } - onDispose { - CallSoundsPlayer.stop() - if (wasConnected.value) { - CallSoundsPlayer.vibrate() - } - callAudioDeviceManager.stop() - dropAudioManagerOverrides() - if (proximityLock?.isHeld == true) { - proximityLock.release() - } - } - } - LaunchedEffect(chatModel.activeCallViewIsCollapsed.value) { + LaunchedEffect(callState, chatModel.activeCallViewIsCollapsed.value) { + callState ?: return@LaunchedEffect if (chatModel.activeCallViewIsCollapsed.value) { - if (proximityLock?.isHeld == true) proximityLock.release() + if (callState.proximityLock?.isHeld == true) callState.proximityLock.release() } else { delay(1000) - if (proximityLock?.isHeld == false) proximityLock.acquire() + if (callState.proximityLock?.isHeld == false) callState.proximityLock.acquire() } } Box(Modifier.fillMaxSize()) { @@ -114,30 +134,31 @@ actual fun ActiveCallView() { 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) 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 - callAudioDeviceManager.start() + callState.callAudioDeviceManager.start() } else { - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.supportsVideo(), true) + 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.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 - callAudioDeviceManager.start() + callState.callAudioDeviceManager.start() } else { - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.supportsVideo(), true) + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) } } is WCallResponse.Answer -> withBGApi { @@ -161,6 +182,17 @@ actual fun ActiveCallView() { is WCallResponse.Connected -> { 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) } } @@ -173,16 +205,23 @@ actual fun ActiveCallView() { updateActiveCall(call) { it.copy(callState = CallState.Negotiated) } is WCallCommand.Media -> { updateActiveCall(call) { - when (cmd.media) { - CallMediaType.Video -> it.copy(videoEnabled = cmd.enable) - CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable) + 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 -> { updateActiveCall(call) { it.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 -> { @@ -199,12 +238,17 @@ actual fun ActiveCallView() { val showOverlay = when { call == null -> false !platform.androidPictureInPictureAllowed() -> true - !call.supportsVideo() -> true !chatModel.activeCallViewIsCollapsed.value -> true else -> false } - if (call != null && showOverlay) { - ActiveCallOverlay(call, chatModel, callAudioDeviceManager) + 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 @@ -212,16 +256,12 @@ actual fun ActiveCallView() { 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) @@ -233,12 +273,18 @@ actual fun ActiveCallView() { private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) { ActiveCallOverlayLayout( call = call, - devices = remember { callAudioDeviceManager.devices }.value, - currentDevice = remember { callAudioDeviceManager.currentDevice }, + 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)) }, + toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) }, selectDevice = { callAudioDeviceManager.selectDevice(it.id) }, - toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) }, + 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 = { 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 } @@ -292,30 +338,33 @@ private fun ActiveCallOverlayLayout( flipCamera: () -> Unit ) { Column { - val media = call.peerMedia ?: call.localMedia - CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) { - if (media == CallMediaType.Video) { - Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) - } - } + 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) + } + }, + onBack = { chatModel.activeCallViewIsCollapsed.value = true } + ) Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { @Composable - fun SelectSoundDevice() { + 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 } ) { val isSpeaker = currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER - ToggleSoundButton(call, enabled, isSpeaker, toggleSound) + ToggleSoundButton(enabled, isSpeaker, !call.peerMediaSources.mic, toggleSound, size = size) } else { ExposedDropDownSettingWithIcon( - devices.map { Triple(it, it.icon, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) }, + 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, - iconSize = 40.dp, + boxSize = size, listIconSize = 30.dp, iconColor = Color(0xFFFFFFD8), + background = controlButtonsBackground(), minWidth = 300.dp, onSelected = { if (it != null) { @@ -326,29 +375,9 @@ private fun ActiveCallOverlayLayout( } } - when (media) { - CallMediaType.Video -> { - VideoCallInfoView(call) - Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { - DisabledBackgroundCallsButton() - } - Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { - ToggleAudioButton(call, enabled, toggleAudio) - SelectSoundDevice() - IconButton(onClick = dismiss, enabled = enabled) { - Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp)) - } - if (call.videoEnabled) { - ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera) - ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo) - } else { - Spacer(Modifier.size(48.dp)) - ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo) - } - } - } - - CallMediaType.Audio -> { + when (call.hasVideo) { + true -> VideoCallInfoView(call) + false -> { Spacer(Modifier.fillMaxHeight().weight(1f)) Column( Modifier.fillMaxWidth(), @@ -358,23 +387,26 @@ private fun ActiveCallOverlayLayout( ProfileImage(size = 192.dp, image = call.contact.profile.image) AudioCallInfoView(call) } - 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, enabled = enabled) { - Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp)) - } - } - Box(Modifier.padding(start = 32.dp)) { - ToggleAudioButton(call, enabled, toggleAudio) - } - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { - Box(Modifier.padding(end = 32.dp)) { - SelectSoundDevice() - } - } + } + } + 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) } } } @@ -383,34 +415,53 @@ private fun ActiveCallOverlayLayout( } @Composable -private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit) { - 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, enabled: Boolean = true, toggleAudio: () -> Unit) { - if (call.audioEnabled) { - ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio) - } else { - ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, 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, speaker: Boolean, toggleSound: () -> Unit) { - if (speaker) { - ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound) +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, enabled, toggleSound) + ControlButton(painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio, size = size) } } +@Composable +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) { @@ -456,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) } } @@ -538,7 +589,7 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni Icon( painterResource(MR.images.ic_call_500), stringResource(MR.strings.permissions_record_audio), - Modifier.size(24.dp), + Modifier.size(22.dp), tint = Color(0xFFFFFFD8) ) } @@ -546,44 +597,46 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni Icon( painterResource(MR.images.ic_videocam), stringResource(MR.strings.permissions_camera), - Modifier.size(24.dp), + Modifier.size(22.dp), tint = Color(0xFFFFFFD8) ) } } } else { - ColumnWithScrollBar(Modifier.fillMaxSize()) { - Spacer(Modifier.height(AppBarHeight)) - - AppBarTitle(stringResource(MR.strings.permissions_required)) - Spacer(Modifier.weight(1f)) - - val onClick = { - if (permissionsState.shouldShowRationale) { - context.showAllowPermissionInSettingsAlert() - } else { - permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert) + 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 "" - GrantPermissionButton(text, buttonEnabled.value, onClick) + 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 = if (hasVideo) 0.dp else DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.Center) { - SimpleButtonFrame(cancel, Modifier.height(64.dp)) { - Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8)) + 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)) + } } } } @@ -669,38 +722,49 @@ fun WebRTCView(callCommand: SnapshotStateList, onResponse: (WVAPIM Box(Modifier.fillMaxSize()) { AndroidView( - factory = { AndroidViewContext -> - (staticWebView ?: WebView(androidAppContext)).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - this.webChromeClient = object: WebChromeClient() { - override fun onPermissionRequest(request: PermissionRequest) { - if (request.origin.toString().startsWith("file:/")) { - request.grant(request.resources) - } else { - Log.d(TAG, "Permission request from webview denied.") - request.deny() + factory = { + try { + (staticWebView ?: WebView(androidAppContext)).apply { + reapplyLocale() + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + this.webChromeClient = object: WebChromeClient() { + override fun onPermissionRequest(request: PermissionRequest) { + if (request.origin.toString().startsWith("file:/")) { + request.grant(request.resources) + } else { + Log.d(TAG, "Permission request from webview denied.") + request.deny() + } } } + this.webViewClient = LocalContentWebViewClient(webView, assetLoader) + 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 + if (staticWebView == null) { + this.loadUrl("file:android_asset/www/android/call.html") + } else { + webView.value = this + } } - this.webViewClient = LocalContentWebViewClient(webView, assetLoader) - 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 - if (staticWebView == null) { - this.loadUrl("file:android_asset/www/android/call.html") + } 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 { - webView.value = this + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString())) } + return@AndroidView View(androidAppContext) } } ) { /* WebView */ } @@ -732,6 +796,16 @@ private fun updateActiveCall(initial: Call, transform: (Call) -> Call) { } } +/* +* 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, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() { override fun shouldInterceptRequest( view: WebView, @@ -761,12 +835,14 @@ fun PreviewActiveCallOverlayVideo() { 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 = {} ), devices = emptyList(), currentDevice = remember { mutableStateOf(null) }, @@ -775,7 +851,7 @@ fun PreviewActiveCallOverlayVideo() { selectDevice = {}, toggleVideo = {}, toggleSound = {}, - flipCamera = {} + flipCamera = {}, ) } } @@ -790,12 +866,14 @@ fun PreviewActiveCallOverlayAudio() { 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 = {} ), devices = emptyList(), currentDevice = remember { mutableStateOf(null) }, 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, + 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 c606e9acb0..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,12 +2,14 @@ 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.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 @@ -21,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 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/chatlist/ChatListNavLinkView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt index f0f733111a..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 @@ -21,6 +21,7 @@ actual fun ChatListNavLinkLayout( nextChatSelected: State, ) { var modifier = Modifier.fillMaxWidth() + if (!disabled) modifier = modifier .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) .onRightClick { showMenu.value = true } 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 4a8b912cdd..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,32 +1,26 @@ package chat.simplex.common.views.chatlist -import android.app.Activity 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.material.ripple.rememberRipple 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.toArgb import androidx.compose.ui.platform.* 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.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.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.datetime.Clock private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp @@ -36,13 +30,27 @@ 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 ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow) { +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) } - Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { + val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) { val source = remember { MutableInteractionSource() } - val indication = rememberRipple(bounded = true, 3000.dp) - Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) { - GreenLine(call) + 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 @@ -50,12 +58,11 @@ actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableState .size(CALL_BOTTOM_ICON_HEIGHT) .background(SimplexGreen, CircleShape) .clip(CircleShape) - .clickable(onClick = onClick, indication = indication, interactionSource = source) + .clickable(onClick = onClick, indication = ripple, interactionSource = source) .align(Alignment.BottomCenter), contentAlignment = Alignment.Center ) { - val media = call.peerMedia ?: call.localMedia - if (media == CallMediaType.Video) { + 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) @@ -65,12 +72,12 @@ actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableState } @Composable -private fun GreenLine(call: Call) { +private fun GreenLine(statusBarHeight: Dp, call: Call) { Row( Modifier .fillMaxSize() .background(SimplexGreen) - .padding(top = -CALL_TOP_OFFSET) + .padding(top = -CALL_TOP_OFFSET + statusBarHeight) .padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center @@ -79,11 +86,10 @@ private fun GreenLine(call: Call) { Spacer(Modifier.weight(1f)) CallDuration(call) } - val window = (LocalContext.current as Activity).window DisposableEffect(Unit) { - window.statusBarColor = SimplexGreen.toArgb() + platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight) onDispose { - window.statusBarColor = Color.Black.toArgb() + platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight) } } } 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, showError: State, emoji: MutableState) { + 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, + 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, 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/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 9ee816bb76..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 @@ -241,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/QRCodeScanner.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt index df38295787..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 @@ -33,6 +33,7 @@ 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 @@ -41,13 +42,13 @@ import java.util.concurrent.* actual fun QRCodeScanner( showQRCodeScanner: MutableState, padding: PaddingValues, - onBarcode: (String) -> Unit + onBarcode: suspend (String) -> Boolean ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - var preview by remember { mutableStateOf(null) } - var lastAnalyzedTimeStamp = 0L - var contactLink = "" + val preview = remember { mutableStateOf(null) } + val contactLink = remember { mutableStateOf("") } + val checkingLink = remember { mutableStateOf(false) } val cameraProviderFuture by produceState?>(initialValue = null) { value = ProcessCameraProvider.getInstance(context) @@ -86,28 +87,33 @@ actual fun QRCodeScanner( .build() val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() cameraProviderFuture?.addListener({ - preview = Preview.Builder().build().also { + preview.value = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } val detector: QrCodeDetector = 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) + 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 -> getQR(proxy) } + val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> withApi { getQR(proxy) } } val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setImageQueueDepth(1) @@ -115,7 +121,7 @@ actual fun QRCodeScanner( .also { it.setAnalyzer(cameraExecutor, imageAnalyzer) } try { cameraProviderFuture?.get()?.unbindAll() - cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) + cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview.value, imageAnalysis) } catch (e: Exception) { Log.d(TAG, "CameraPreview: ${e.localizedMessage}") } 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 a39e71947d..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,19 +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.PermissionStatus -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.status == PermissionStatus.Granted) { - if (notificationsPermissionState.status == PermissionStatus.Granted) { - ntfManager.androidCreateNtfChannelsMaybeShowAlert() + 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, 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 5a60e1d1b0..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 @@ -13,20 +14,17 @@ 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.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.* +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.* @@ -34,7 +32,7 @@ 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 @@ -46,9 +44,8 @@ enum class AppIcon(val image: ImageResource) { } @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()}") @@ -65,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) - } - }, ) } @@ -86,14 +76,10 @@ fun AppearanceScope.AppearanceLayout( languagePref: SharedPreference, systemDarkTheme: SharedPreference, changeIcon: (AppIcon) -> Unit, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + 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( @@ -119,10 +105,30 @@ 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] @@ -131,7 +137,8 @@ fun AppearanceScope.AppearanceLayout( 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) @@ -146,10 +153,8 @@ fun AppearanceScope.AppearanceLayout( } SectionDividerSpaced(maxTopPadding = true) - ProfileImageSection() + FontScaleSection() - SectionDividerSpaced(maxTopPadding = true) - ThemesSection(systemDarkTheme, showSettingsModal, editColor) SectionBottomSpacer() } } @@ -169,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 f2c3d86ab5..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,7 +2,6 @@ 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.* @@ -14,14 +13,13 @@ import dev.icerock.moko.resources.compose.stringResource @Composable actual fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable ModalData.(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) } } @@ -33,7 +31,7 @@ fun restartApp() { } 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 @@ - + 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 e7dda42ade..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,23 +1,26 @@ package chat.simplex.common +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.Animatable 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.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.Color -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 @@ -28,6 +31,8 @@ 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.* @@ -37,17 +42,37 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -data class SettingsViewState( - val userPickerState: MutableStateFlow, - val scaffoldState: ScaffoldState -) - @Composable fun AppScreen() { + AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } SimpleXTheme { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { - 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() + } + } } } } @@ -66,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.size > 2 + && chatModel.chats.size > 3 && chatModel.activeCallInvitation.value == null ) { AppLock.showLANotice(ChatModel.controller.appPrefs.laNoticeShown) } @@ -78,6 +104,7 @@ fun MainScreen() { laUnavailableInstructionAlert() } } + platform.desktopShowAppUpdateNotice() LaunchedEffect(chatModel.clearOverlays.value) { if (chatModel.clearOverlays.value) { ModalManager.closeAllModalsEverywhere() @@ -87,7 +114,7 @@ fun MainScreen() { @Composable fun AuthView() { - Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { + Surface(color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { Box( Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -135,30 +162,53 @@ 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() } @@ -173,7 +223,7 @@ fun MainScreen() { if (chatModel.controller.appPrefs.performLA.get() && AppLock.laFailed.value) { AuthView() } else { - SplashView() + SplashView(true) ModalManager.fullscreen.showPasscodeInView() } } else { @@ -189,10 +239,8 @@ fun MainScreen() { } else { ActiveCallView() } - } else { - // It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked - ModalManager.fullscreen.showPasscodeInView() } + ModalManager.fullscreen.showOneTimePasscodeInView() AlertManager.privacySensitive.showInView() if (onboarding == OnboardingStage.OnboardingComplete) { LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) { @@ -228,20 +276,40 @@ 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) { BoxWithConstraints { - val call = remember { chatModel.activeCall} .value - val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted - 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() } - .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) ) { - StartPartOfScreen(settingsState) + StartPartOfScreen(userPickerState) } val scope = rememberCoroutineScope() val onComposed: suspend (chatId: String?) -> Unit = { chatId -> @@ -262,42 +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() } - .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) + .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier) + .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } ) Box2@{ - currentChatId?.let { - ChatView(it, chatModel, onComposed) + currentChatId.value?.let { + ChatView(chatsCtx = chatModel.chatsContext, currentChatId, onComposed = onComposed) } } - if (call != null && showCallArea) { - ActiveCallInteractiveArea(call, remember { MutableStateFlow(AnimatedViewState.GONE) }) - } } } @Composable -fun StartPartOfScreen(settingsState: SettingsViewState) { +fun StartPartOfScreen(userPickerState: MutableStateFlow) { 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) { @@ -305,7 +378,7 @@ fun CenterPartOfScreen() { } } } - when (val id = currentChatId) { + when (currentChatId.value) { null -> { if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { Box( @@ -320,7 +393,7 @@ fun CenterPartOfScreen() { ModalManager.center.showInView() } } - else -> ChatView(id, chatModel) {} + else -> ChatView(chatsCtx = chatModel.chatsContext, currentChatId) {} } } @@ -330,49 +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) { + 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 - } + UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA) } - ModalManager.fullscreen.showInView() } + 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 d6214c252c..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 @@ -5,6 +5,7 @@ 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 @@ -31,7 +32,7 @@ object AppLock { fun showLANotice(laNoticeShown: SharedPreference) { 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), @@ -57,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, @@ -80,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 } @@ -104,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, contentColor = LocalContentColor.current) { + 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 @@ -147,6 +155,7 @@ object AppLock { else generalGetString(MR.strings.auth_unlock), selfDestruct = true, + oneTime = false, completed = { laResult -> when (laResult) { LAResult.Success -> @@ -160,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() } @@ -192,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() } @@ -227,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) @@ -240,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 3838cd45a6..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 @@ -4,14 +4,17 @@ 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 @@ -19,8 +22,9 @@ 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.* @@ -28,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.* @@ -52,19 +59,33 @@ object ChatModel { val chatDbStatus = mutableStateOf(null) val ctrlInitInProgress = mutableStateOf(false) val dbMigrationInProgress = mutableStateOf(false) - val chats = mutableStateListOf() + val incompleteInitializedDbRemoved = mutableStateOf(false) // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() val switchingUsersAndHosts = mutableStateOf(false) // current chat val chatId = mutableStateOf(null) - val chatItems = mutableStateOf(SnapshotStateList()) + val openAroundItemId: MutableState = mutableStateOf(null) + val chatsContext = ChatsContext(null) + val secondaryChatsContext = mutableStateOf(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> = chatsContext.chats // rhId, chatId val deletedChats = mutableStateOf>>(emptyList()) - val chatItemStatuses = mutableMapOf() - val groupMembers = mutableStateListOf() + val groupMembers = mutableStateOf>(emptyList()) + val groupMembersIndexes = mutableStateOf>(emptyMap()) + val membersLoaded = mutableStateOf(false) + // Chat Tags + val userTags = mutableStateOf(emptyList()) + val activeChatTagFilter = mutableStateOf(null) + val presetTags = mutableStateMapOf() + val unreadTags = mutableStateMapOf() + + // 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() val terminalItems = mutableStateOf>(listOf()) val userAddress = mutableStateOf(null) val chatItemTTL = mutableStateOf(ChatItemTTL.None) @@ -76,7 +97,13 @@ object ChatModel { val desktopOnboardingRandomPassword = mutableStateOf(false) // set when app is opened via contact or invitation URI (rhId, uri) - val appOpenUrl = mutableStateOf?>(null) + val appOpenUrl = mutableStateOf?>(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 { @@ -88,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()) } @@ -120,7 +147,11 @@ object ChatModel { val clipboardHasText = mutableStateOf(false) val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true)) - val updatingChatsMutex: Mutex = Mutex() + 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 @@ -137,6 +168,9 @@ object ChatModel { val processedCriticalError: ProcessedErrors = ProcessedErrors(60_000) val processedInternalError: ProcessedErrors = 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 { @@ -163,227 +197,716 @@ object ChatModel { } } - // 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() + val newUnreadTags = mutableMapOf() - 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 = 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?, newTags: List) { + 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()) + /** 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()) + val chatItemStatuses = mutableMapOf() + // 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() + 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) { - 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) { + 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().apply { addAll(chatItems.value); add(index, elem); chatState.itemAdded(elem.id to elem.isRcvNew) } + } + + fun addToChatItems(elem: ChatItem) { + chatItems.value = SnapshotStateList().apply { addAll(chatItems.value); add(elem); chatState.itemAdded(elem.id to elem.isRcvNew) } + } + + fun removeLastChatItems() { + val removed: Triple + chatItems.value = SnapshotStateList().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))) - } - 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) { - chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) - } else { - chatItems.add(cItem) + 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 } - 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) { + 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() + private var remoteHostId: Long? = null + private val chatsToPop = mutableMapOf() + + 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 { + val chs = mutableListOf() + // 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? = 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? = null): Pair { + var markedRead = 0 + var mentionsMarkedRead = 0 + if (chatId.value == id) { val items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - items[itemIndex] = cItem + var i = items.lastIndex + val itemIdsFromRange = itemIds?.toMutableSet() ?: mutableSetOf() + val markedReadIds = mutableSetOf() + 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) + 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 items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - items[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) { + // 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) { - chatItems.removeAll { - val remove = it.id == cItem.id - if (remove) { AudioPlayer.stop(it) } - remove - } - } - } - - 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) { @@ -399,108 +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.value.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 - val items = chatItems.value - while (i < items.size) { - val item = items[i] - if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) { - 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))) - ) - } - markedRead++ + if (chatsContext.chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + withApi { + withContext(Dispatchers.Main) { + chatsContext.removeLastChatItems() } - i += 1 } } - 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): 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> { + fun getConnectedMemberNames(cItem: ChatItem, reversedChatItems: List): Pair> { var count = 0 val ns = mutableListOf() - 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 @@ -515,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 { - 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 { + fun getPrevShownChatItem(ciIndex: Int?, ciCategory: CIMergeCategory?, reversedChatItems: List): Pair { var i = ciIndex ?: return null to null - val reversedChatItems = chatItems.asReversed() val fst = reversedChatItems.lastIndex while (i < fst) { i++ @@ -543,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 { - val reversedChatItems = chatItems.asReversed() + fun getPrevHiddenMember(member: GroupMember, range: IntRange, reversedChatItems: List): Pair { var prevMember: GroupMember? = null val names: MutableSet = mutableSetOf() for (i in range) { @@ -566,26 +1015,29 @@ object ChatModel { // } // } - private fun popChat_(i: Int) { - val chat = chats.removeAt(i) - chats.add(index = 0, chat) - } - fun replaceConnReqView(id: String, withId: String) { if (id == showingInvitation.value?.connId) { - showingInvitation.value = null - chatModel.chatItems.clear() - chatModel.chatId.value = withId + withApi { + withContext(Dispatchers.Main) { + showingInvitation.value = null + chatsContext.chatItems.clearAndNotify() + chatModel.chatId.value = withId + } + } + ModalManager.start.closeModals() ModalManager.end.closeModals() } } - fun dismissConnReqView(id: String) { + fun dismissConnReqView(id: String) = withApi { if (id == showingInvitation.value?.connId) { - showingInvitation.value = null - chatModel.chatItems.clear() - chatModel.chatId.value = null + withContext(Dispatchers.Main) { + showingInvitation.value = null + chatsContext.chatItems.clearAndNotify() + chatModel.chatId.value = null + } // Close NewChatView + ModalManager.start.closeModals() ModalManager.center.closeModals() ModalManager.end.closeModals() } @@ -595,40 +1047,6 @@ object ChatModel { showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true) } - 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 - } - } else { - false - } - } - - 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 setContactNetworkStatus(contact: Contact, status: NetworkStatus) { val conn = contact.activeConn if (conn != null) { @@ -646,6 +1064,16 @@ object ChatModel { fun addTerminalItem(item: TerminalItem) { 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) } @@ -658,8 +1086,9 @@ object ChatModel { data class ShowingInvitation( val connId: String, - val connReq: String, - val connChatUsed: Boolean + val connLink: CreatedConnLink, + val connChatUsed: Boolean, + val conn: PendingContactConnection ) enum class ChatType(val type: String) { @@ -679,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 @@ -705,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, ) } } @@ -757,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 { @@ -765,8 +1203,9 @@ interface SomeChat { val id: ChatId val apiId: Long val ready: Boolean + val chatDeleted: Boolean + val userCantSendReason: Pair? val sendMsgEnabled: Boolean - val ntfsEnabled: Boolean val incognito: Boolean fun featureEnabled(feature: ChatFeature): Boolean val timedMessagesTTL: Int? @@ -781,13 +1220,8 @@ data class Chat( val chatItems: List, val chatStats: ChatStats = ChatStats() ) { - val userCanSend: Boolean - get() = when (chatInfo) { - is ChatInfo.Direct -> true - is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member - is ChatInfo.Local -> true - else -> false - } + @Transient + var popTs: Instant? = null val nextSendGrpInv: Boolean get() = when (chatInfo) { @@ -795,36 +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) { - val groupInfo = chatInfo.groupInfo - val p = groupInfo.fullGroupPreferences - when (feature) { - GroupFeature.TimedMessages -> p.timedMessages.on - GroupFeature.DirectMessages -> p.directMessages.on(groupInfo.membership) - GroupFeature.FullDelete -> p.fullDelete.on - GroupFeature.Reactions -> p.reactions.on - GroupFeature.Voice -> p.voice.on(groupInfo.membership) - GroupFeature.Files -> p.files.on(groupInfo.membership) - GroupFeature.SimplexLinks -> p.simplexLinks.on(groupInfo.membership) - GroupFeature.History -> p.history.on - } + 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( @@ -845,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 @@ -856,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) @@ -869,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 @@ -893,8 +1325,9 @@ sealed class ChatInfo: SomeChat, NamedChat { 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 ntfsEnabled get() = noteFolder.ntfsEnabled override val incognito get() = noteFolder.incognito override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature) override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL @@ -917,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 @@ -941,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 @@ -960,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 @@ -980,9 +1418,20 @@ 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 @@ -999,7 +1448,24 @@ sealed class ChatInfo: SomeChat, NamedChat { is ContactConnection -> contactConnection.updatedAt is InvalidJSON -> updatedAt } -} + + val chatTags: List? + 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 { @@ -1041,18 +1507,30 @@ data class Contact( override val updatedAt: Instant, val chatTs: Instant?, val contactGroupMemberId: Long? = null, - val contactGrpInvSent: Boolean + val contactGrpInvSent: Boolean, + val chatTags: List, + 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? + 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 @@ -1069,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 @@ -1109,15 +1593,26 @@ data class Contact( createdAt = Clock.System.now(), updatedAt = Clock.System.now(), chatTs = Clock.System.now(), - contactGrpInvSent = false + 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 @@ -1150,24 +1645,28 @@ data class Connection( val pqEncryption: Boolean, val pqSndEnabled: Boolean? = null, val pqRcvEnabled: Boolean? = null, - val connectionStats: ConnectionStats? = 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, pqSupport = false, pqEncryption = false) + 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) @@ -1234,25 +1733,53 @@ class Group ( var members: List ) +@Serializable +sealed class ForwardConfirmation { + @Serializable @SerialName("filesNotAccepted") data class FilesNotAccepted(val fileIds: List) : 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, - val chatTs: Instant? + val chatTs: Instant?, + val uiThemes: ThemeModeOverrides? = null, + val chatTags: List, + 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? 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 @@ -1262,12 +1789,11 @@ data class GroupInfo ( 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 @@ -1276,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, @@ -1283,11 +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(), - chatTs = Clock.System.now() + chatTs = Clock.System.now(), + uiThemes = null, + chatTags = emptyList(), + localAlias = "", + chatItemTTL = null ) } } @@ -1312,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, @@ -1328,21 +1888,30 @@ data class GroupMember ( val memberContactId: Long? = null, val memberContactProfileId: Long, var activeConn: Connection? = null -) { +): NamedChat { val id: String get() = "#$groupId @$groupMemberId" - val displayName: String + 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) } - val fullName: String get() = memberProfile.fullName - val image: String? get() = memberProfile.image + 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 + 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}") } @@ -1356,12 +1925,27 @@ data class GroupMember ( 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 @@ -1372,11 +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 @@ -1395,13 +1981,13 @@ data class GroupMember ( fun canChangeRoleTo(groupInfo: GroupInfo): List? = 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.Admin - && userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive + return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Moderator + && userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive } val memberIncognito = memberProfile.profileId != memberContactProfileId @@ -1446,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 = 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) } @@ -1469,11 +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, @@ -1483,11 +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) @@ -1498,11 +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) @@ -1555,9 +2153,10 @@ class NoteFolder( 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? = null override val sendMsgEnabled get() = true - override val ntfsEnabled get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice override val timedMessagesTTL: Int? get() = null @@ -1591,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 @@ -1622,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 @@ -1630,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 @@ -1645,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 ) } } @@ -1688,6 +2290,7 @@ class PendingContactConnection( @Serializable enum class ConnStatus { @SerialName("new") New, + @SerialName("prepared") Prepared, @SerialName("joined") Joined, @SerialName("requested") Requested, @SerialName("accepted") Accepted, @@ -1697,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, @@ -1718,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, @@ -1732,6 +2372,7 @@ data class ChatItem ( val meta: CIMeta, val content: CIContent, val formattedText: List? = null, + val mentions: Map? = null, val quotedItem: CIQuote? = null, val reactions: List, val file: CIFile? = null @@ -1826,19 +2467,46 @@ data class ChatItem ( } } - fun memberToModerate(chatInfo: ChatInfo): Pair? { + fun memberToModerate(chatInfo: ChatInfo): Pair? { 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 } } + 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 -> false @@ -1900,18 +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, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, 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(), @@ -1993,6 +2663,7 @@ 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, @@ -2001,7 +2672,8 @@ data class ChatItem ( itemTimed = null, itemLive = false, deletable = false, - editable = false + editable = false, + userMention = false, ), content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast), quotedItem = null, @@ -2016,6 +2688,7 @@ data class ChatItem ( itemTs = Clock.System.now(), itemText = "", itemStatus = CIStatus.RcvRead(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), itemForwarded = null, @@ -2024,7 +2697,8 @@ data class ChatItem ( itemTimed = null, itemLive = true, deletable = false, - editable = false + editable = false, + userMention = false, ), content = CIContent.SndMsgContent(MsgContent.MCText("")), quotedItem = null, @@ -2044,45 +2718,82 @@ data class ChatItem ( } } -fun MutableState>.add(index: Int, chatItem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(index, chatItem) } +fun MutableState>.add(index: Int, elem: Chat) { + value = SnapshotStateList().apply { addAll(value); add(index, elem) } } -fun MutableState>.add(chatItem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(chatItem) } +fun MutableState>.add(elem: Chat) { + value = SnapshotStateList().apply { addAll(value); add(elem) } } -fun MutableState>.addAll(index: Int, chatItems: List) { - value = SnapshotStateList().apply { addAll(value); addAll(index, chatItems) } +// For some reason, Kotlin version crashes if the list is empty +fun MutableList.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmpty()) false else remAll(predicate) + +fun MutableState>.addAll(index: Int, elems: List) { + value = SnapshotStateList().apply { addAll(value); addAll(index, elems) } } -fun MutableState>.addAll(chatItems: List) { - value = SnapshotStateList().apply { addAll(value); addAll(chatItems) } +fun MutableState>.addAll(elems: List) { + value = SnapshotStateList().apply { addAll(value); addAll(elems) } } -fun MutableState>.removeAll(block: (ChatItem) -> Boolean) { - value = SnapshotStateList().apply { addAll(value); removeAll(block) } +fun MutableState>.removeAll(block: (Chat) -> Boolean) { + value = SnapshotStateList().apply { addAll(value); removeAll(block) } } -fun MutableState>.removeAt(index: Int) { - value = SnapshotStateList().apply { addAll(value); removeAt(index) } +// TODO [contexts] operates with both contexts? +// Removes item(s) from chatItems and notifies a listener about removed item(s) +fun MutableState>.removeAllAndNotify(block: (ChatItem) -> Boolean) { + val toRemove = ArrayList>() + value = SnapshotStateList().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>.removeLast() { - value = SnapshotStateList().apply { addAll(value); removeLast() } +fun MutableState>.removeAt(index: Int): Chat { + val new = SnapshotStateList() + new.addAll(value) + val res = new.removeAt(index) + value = new + return res } -fun MutableState>.replaceAll(chatItems: List) { - value = SnapshotStateList().apply { addAll(chatItems) } +fun MutableState>.replaceAll(elems: List) { + value = SnapshotStateList().apply { addAll(elems) } } -fun MutableState>.clear() { - value = SnapshotStateList() +fun MutableState>.clear() { + value = SnapshotStateList() } -fun State>.asReversed(): MutableList = value.asReversed() +// TODO [contexts] operates with both contexts? +// Removes all chatItems and notifies a listener about it +fun MutableState>.clearAndNotify() { + value = SnapshotStateList() + chatModel.chatsContext.chatState.clear() + chatModel.secondaryChatsContext.value?.chatState?.clear() +} -val State>.size: Int get() = value.size +fun State>.asReversed(): MutableList = value.asReversed() + +fun State>.toList(): List = value.toList() + +operator fun State>.get(i: Int): T = value[i] + +operator fun State>.set(index: Int, elem: T) { value[index] = elem } + +val State>.size: Int get() = value.size enum class CIMergeCategory { MemberConnected, @@ -2118,6 +2829,7 @@ data class CIMeta ( val itemTs: Instant, val itemText: String, val itemStatus: CIStatus, + val sentViaProxy: Boolean?, val createdAt: Instant, val updatedAt: Instant, val itemForwarded: CIForwardedFrom?, @@ -2125,10 +2837,12 @@ data class CIMeta ( 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 @@ -2144,7 +2858,7 @@ data class CIMeta ( companion object { fun getSample( - id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), + 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 = @@ -2153,6 +2867,7 @@ data class CIMeta ( itemTs = ts, itemText = text, itemStatus = status, + sentViaProxy = sentViaProxy, createdAt = ts, updatedAt = ts, itemForwarded = itemForwarded, @@ -2161,7 +2876,8 @@ data class CIMeta ( itemTimed = itemTimed, itemLive = itemLive, deletable = deletable, - editable = editable + editable = editable, + userMention = false, ) fun invalidJSON(): CIMeta = @@ -2171,6 +2887,7 @@ 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, @@ -2179,7 +2896,8 @@ data class CIMeta ( itemTimed = null, itemLive = false, deletable = false, - editable = false + editable = false, + userMention = false ) } } @@ -2190,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) @@ -2198,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) } @@ -2221,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() @@ -2251,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 @@ -2262,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, @@ -2281,6 +3061,48 @@ 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 = + 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? 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() @@ -2308,6 +3130,13 @@ sealed class CIForwardedFrom { is Group -> chatName } + val chatTypeApiIdMsgId: Triple? + 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()) { @@ -2323,6 +3152,7 @@ sealed class CIForwardedFrom { @Serializable enum class CIDeleteMode(val deleteMode: String) { @SerialName("internal") cidmInternal("internal"), + @SerialName("internalMark") cidmInternalMark("internalMark"), @SerialName("broadcast") cidmBroadcast("broadcast"); } @@ -2442,6 +3272,7 @@ sealed class CIContent: ItemContent { 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) } @@ -2519,11 +3350,21 @@ sealed class MsgReaction { MREmojiChar.Heart -> "❤️" else -> emoji.value } - is Unknown -> "" + is Unknown -> "?" } companion object { - val values: List get() = MREmojiChar.values().map(::Emoji) + val values: List get() = MREmojiChar.entries.map(::Emoji) + val supported: List get() = listOf( + MREmojiChar.ThumbsUp, + MREmojiChar.ThumbsDown, + MREmojiChar.Smile, + MREmojiChar.Laugh, + MREmojiChar.Sad, + MREmojiChar.Heart, + MREmojiChar.Launch, + MREmojiChar.Check + ).map(::Emoji) } } @@ -2541,8 +3382,13 @@ object MsgReactionSerializer : KSerializer { return if (json is JsonObject && "type" in json) { when(val t = json["type"]?.jsonPrimitive?.content ?: "") { "emoji" -> { - val emoji = Json.decodeFromString(json["emoji"].toString()) - if (emoji == null) MsgReaction.Unknown(t, json) else MsgReaction.Emoji(emoji) + val msgReaction = try { + val emoji = Json.decodeFromString(json["emoji"].toString()) + MsgReaction.Emoji(emoji) + } catch (e: Throwable) { + MsgReaction.Unknown(t, json) + } + msgReaction } else -> MsgReaction.Unknown(t, json) } @@ -2570,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 @@ -2590,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 } @@ -2611,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 @@ -2653,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, @@ -2784,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) { @@ -2799,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? 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 { @@ -2820,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)}" } @@ -2882,6 +3803,10 @@ object MsgContentSerializer : KSerializer { element("MCFile", buildClassSerialDescriptor("MCFile") { element("text") }) + element("MCReport", buildClassSerialDescriptor("MCReport") { + element("text") + element("reason") + }) element("MCUnknown", buildClassSerialDescriptor("MCUnknown")) } @@ -2912,6 +3837,10 @@ object MsgContentSerializer : KSerializer { MsgContent.MCVoice(text, duration) } "file" -> MsgContent.MCFile(text) + "report" -> { + val reason = Json.decodeFromString(json["reason"].toString()) + MsgContent.MCReport(text, reason) + } else -> MsgContent.MCUnknown(t, text, json) } } else { @@ -2960,17 +3889,34 @@ object MsgContentSerializer : KSerializer { 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" @@ -2983,6 +3929,10 @@ class FormattedText(val text: String, val format: Format? = null) { fun simplexLinkText(linkType: SimplexLinkType, smpHosts: List): String = "${linkType.description} (${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})" + + companion object { + fun plain(text: String): List = if (text.isEmpty()) emptyList() else listOf(FormattedText(text)) + } } @Serializable @@ -2995,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): Format() + @Serializable @SerialName("mention") class Mention(val memberName: String): Format() @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() @@ -3007,6 +3958,7 @@ 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 } @@ -3022,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 }) } @@ -3054,6 +4008,58 @@ 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 = 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 { + 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() {} @@ -3298,37 +4304,61 @@ enum class SwitchPhase { @SerialName("completed") Completed } -sealed class ChatItemTTL: Comparable { +sealed class ChatItemTTL: Comparable { 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, @@ -3348,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 b71610597e..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,42 +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 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.updatingChatsMutex 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.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, @@ -66,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 = SharedPreference( - get = fun(): CallOnLockScreen { - val value = _callOnLockScreen.get() ?: return CallOnLockScreen.default - return try { - CallOnLockScreen.valueOf(value) - } catch (e: Throwable) { - CallOnLockScreen.default - } - }, - set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) } - ) + val callOnLockScreen: SharedPreference = 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) @@ -90,22 +100,18 @@ 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 = SharedPreference( - get = fun(): SimplexLinkMode { - val value = _simplexLinkMode.get() ?: return SimplexLinkMode.default - return try { - SimplexLinkMode.valueOf(value) - } catch (e: Throwable) { - 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 = 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) @@ -113,25 +119,34 @@ class AppPreferences { 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 = 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 = SharedPreference( + get = fun(): NetworkProxy { + val value = _networkProxy.get() ?: return NetworkProxy() return try { - TransportSessionMode.valueOf(value) + if (value.startsWith("{")) { + json.decodeFromString(value) + } else { + NetworkProxy(host = value.substringBefore(":").ifBlank { "localhost" }, port = value.substringAfter(":").toIntOrNull() ?: 9050) + } } catch (e: Throwable) { - TransportSessionMode.default + 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 = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default) + val networkSMPProxyMode: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, SMPProxyMode.default) + val networkSMPProxyFallback: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, SMPProxyFallback.default) + val networkHostMode: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.default) val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) + val networkSMPWebPortServers: SharedPreference = 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) @@ -145,8 +160,13 @@ class AppPreferences { val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false) 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) @@ -164,14 +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) @@ -185,11 +227,28 @@ class AppPreferences { 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, 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), @@ -242,7 +301,19 @@ class AppPreferences { set = fun(value) = settings.putString(prefName, value.toString()) ) - // LALAL + private inline fun > mkSafeEnumPreference(key: String, default: T): SharedPreference = SharedPreference( + get = { + val value = settings.getString(key, "") + if (value == "") return@SharedPreference default + try { + enumValueOf(value) + } catch (e: IllegalArgumentException) { + default + } + }, + set = { value -> settings.putString(key, value.name) } + ) + private fun mkDatePreference(prefName: String, default: Instant?): SharedPreference = SharedPreference( get = { @@ -263,6 +334,12 @@ class AppPreferences { set = fun(value) = prefs.putString(prefName, encode(value)) ) + private fun mkThemeOverridesPreference(): SharedPreference> = + 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" @@ -270,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" @@ -283,31 +361,44 @@ 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" @@ -321,6 +412,8 @@ class AppPreferences { private const val SHARED_PREFS_INCOGNITO = "Incognito" 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" @@ -331,14 +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" @@ -348,9 +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? = null } } @@ -360,7 +468,7 @@ object ChatController { var ctrl: ChatCtrl? = -1 val appPrefs: AppPreferences by lazy { AppPreferences() } - val messagesChannel: Channel = Channel() + val messagesChannel: Channel = Channel() val chatModel = ChatModel private var receiverStarted = false @@ -369,6 +477,26 @@ object ChatController { fun hasChatCtrl() = ctrl != -1L && ctrl != null + suspend fun getAgentSubsTotal(rh: Long?): Pair? { + 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) { @@ -383,12 +511,11 @@ object ChatController { Log.d(TAG, "user: $user") try { apiSetNetworkConfig(getNetCfg()) - val justStarted = apiStartChat() - appPrefs.chatStopped.set(false) + 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) @@ -401,12 +528,14 @@ object ChatController { } 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") } + apiStartChat() + appPrefs.chatStopped.set(false) } catch (e: Throwable) { Log.e(TAG, "failed starting chat $e") throw e @@ -434,9 +563,17 @@ object ChatController { 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) - apiSetNetworkConfig(netCfg, ctrl) - apiSetTempFolder(getMigrationTempFilesDirectory().absolutePath, ctrl) - apiSetFilesFolder(getMigrationTempFilesDirectory().absolutePath, 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 } @@ -451,11 +588,15 @@ object ChatController { } 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) @@ -470,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() { @@ -498,11 +642,11 @@ object ChatController { messagesChannel.trySend(msg) } if (finishedWithoutTimeout == null) { - Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType) + 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.resp.responseType + "\n" + Exception().stackTraceToString()), + text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()), shareText = true ) } @@ -518,40 +662,45 @@ object ChatController { } } - suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null): CR { + 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(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 } } - 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(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?, ctrl: ChatCtrl? = null): User? { val r = sendCmd(rh, CC.ShowActiveUser(), ctrl) - 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, "apiGetActiveUser: ${r.responseType} ${r.details}") if (rh == null) { chatModel.localUserCreated.value = false @@ -559,16 +708,17 @@ object ChatController { return null } - suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? { - val r = sendCmd(rh, CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp), ctrl) - 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 ( - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.InvalidDisplayName + 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 { @@ -580,8 +730,8 @@ object ChatController { suspend fun listUsers(rh: Long?): List { 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}") @@ -590,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}") } @@ -627,124 +777,174 @@ 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(ctrl: ChatCtrl? = null): Boolean { val r = sendCmd(null, CC.StartChat(mainApp = true), ctrl) - when (r) { + when (r.result) { is CR.ChatStarted -> return true is CR.ChatRunning -> return false else -> throw Exception("failed starting chat: ${r.responseType} ${r.details}") } } - suspend fun apiStopChat(): Boolean { - val r = sendCmd(null, CC.ApiStopChat()) - when (r) { - is CR.ChatStopped -> return true - else -> throw Exception("failed stopping 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 apiSetTempFolder(tempFolder: String, ctrl: ChatCtrl? = null) { - val r = sendCmd(null, CC.SetTempFolder(tempFolder), ctrl) - if (r is CR.CmdOk) return - throw Exception("failed to set temp folder: ${r.responseType} ${r.details}") + suspend fun apiStopChat(): Boolean { + val r = sendCmd(null, CC.ApiStopChat()) + if (r.result is CR.ChatStopped) return true + throw Exception("failed stopping chat: ${r.responseType} ${r.details}") } - suspend fun apiSetFilesFolder(filesFolder: String, ctrl: ChatCtrl? = null) { - val r = sendCmd(null, CC.SetFilesFolder(filesFolder), ctrl) - if (r is CR.CmdOk) return - throw Exception("failed to set files folder: ${r.responseType} ${r.details}") - } - - suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) { - val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder)) - if (r is CR.CmdOk) return - throw Exception("failed to set remote hosts folder: ${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 apiSaveAppSettings(settings: AppSettings) { val r = sendCmd(null, CC.ApiSaveSettings(settings)) - if (r is CR.CmdOk) return + 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 CR.AppSettingsR) return r.appSettings + 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) { + suspend fun apiExportArchive(config: ArchiveConfig): List { val r = sendCmd(null, CC.ApiExportArchive(config)) - if (r is CR.CmdOk) return + 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 { val r = sendCmd(null, CC.ApiImportArchive(config)) - if (r is CR.ArchiveImported) return r.archiveErrors + 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 + 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): CR.ChatCmdError? { + suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): ChatError? { val r = sendCmd(null, CC.TestStorageEncryption(key), ctrl) - 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 test storage encryption: ${r.responseType} ${r.details}") } suspend fun apiGetChats(rh: Long?): List { 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?{ + 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? { + 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? { + 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): Pair, List>? { + 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) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds)) + + suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List): List? { + val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages) return processSendMessageCmd(rh, cmd) } - private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): AChatItem? { + private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): List? { 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("processSendMessageCmd", generalGetString(MR.strings.error_sending_message), r) @@ -753,143 +953,234 @@ object ChatController { } } } - suspend fun apiCreateChatItem(rh: Long?, noteFolderId: Long, file: CryptoFile? = null, mc: MsgContent): AChatItem? { - val cmd = CC.ApiCreateChatItem(noteFolderId, file, mc) + + suspend fun apiCreateChatItems(rh: Long?, noteFolderId: Long, composedMessages: List): List? { + val cmd = CC.ApiCreateChatItems(noteFolderId, composedMessages) val r = sendCmd(rh, cmd) - return when (r) { - is CR.NewChatItem -> r.chatItem - else -> { - apiErrorAlert("apiCreateChatItem", generalGetString(MR.strings.error_creating_message), r) - null - } - } + 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 apiReportMessage(rh: Long?, groupId: Long, chatItemId: Long, reportReason: ReportReason, reportText: String): List? { + 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? { - 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 + 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, ttl: Int?): List? { + 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): 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 } } - } - suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long): ChatItem? { - val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId) - return processSendMessageCmd(rh, cmd)?.chatItem - } - - - - 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 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? { + 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, mode: CIDeleteMode): List? { + 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? { - 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): List? { + 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): 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, mode: CIDeleteMode): List? { + 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): 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? { + 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): 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): List? { + 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? { + 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): 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, ctrl: ChatCtrl? = null): Boolean { - val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl) - 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 = @@ -900,154 +1191,199 @@ object ChatController { suspend fun apiContactInfo(rh: Long?, contactId: Long): Pair? { 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? { 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? { + 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? { + 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? { 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? { 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? { 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? { 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? { 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? { - 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? { - 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?, (() -> Unit)?> { + suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair?, (() -> Unit)?> { val userId = try { currentUserId("apiAddContact") } catch (e: Exception) { return null to null } - val r = sendCmd(rh, CC.APIAddContact(userId, incognito)) - return when (r) { - is CR.Invitation -> (r.connReqInvitation to r.connection) to null - else -> { - if (!(networkErrorAlert(r))) { - return null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) } - } - 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}") + 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) + } return null } - suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? { + 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? { 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}") + 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, connReq: String): PendingContactConnection? { + 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, connReq)) + 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 } } } @@ -1055,31 +1391,30 @@ object ChatController { suspend fun apiConnectContactViaAddress(rh: Long?, incognito: Boolean, contactId: Long): Contact? { 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 deleteChat(chat: Chat, notify: Boolean? = null) { - val cInfo = chat.chatInfo - if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) { - chatModel.removeChat(chat.remoteHostId, cInfo.id) - } - } - - suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean { + 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, notify)) + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) + val res = r.result val success = when { - r is CR.ContactDeleted && type == ChatType.Direct -> true - r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true - r is CR.GroupDeletedUser && type == ChatType.Group -> true + 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 @@ -1096,11 +1431,31 @@ object ChatController { 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) { - chatModel.clearChat(chat.remoteHostId, updatedChatInfo) + 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() } @@ -1109,7 +1464,7 @@ object ChatController { 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 } @@ -1117,9 +1472,9 @@ object ChatController { suspend fun apiUpdateProfile(rh: Long?, profile: Profile): Pair>? { 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}") @@ -1128,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 } @@ -1181,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 } @@ -1194,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 } @@ -1207,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) @@ -1228,82 +1601,89 @@ 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 { val r = sendCmd(rh, CC.ApiGetCallInvitations()) - if (r is CR.CallInvitations) return r.callInvitations + 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? { 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): 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 uploadStandaloneFile(user: UserLike, file: CryptoFile, ctrl: ChatCtrl? = null): Pair { val r = sendCmd(null, CC.ApiUploadStandaloneFile(user.userId, file), ctrl) - return if (r is CR.SndStandaloneFileCreated) { - r.fileTransferMeta to null + 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() @@ -1312,8 +1692,8 @@ object ChatController { suspend fun downloadStandaloneFile(user: UserLike, url: String, file: CryptoFile, ctrl: ChatCtrl? = null): Pair { val r = sendCmd(null, CC.ApiDownloadStandaloneFile(user.userId, url, file), ctrl) - return if (r is CR.RcvStandaloneFileCreated) { - r.rcvFileTransfer to null + 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() @@ -1322,44 +1702,140 @@ object ChatController { suspend fun standaloneFileInfo(url: String, ctrl: ChatCtrl? = null): MigrationFileLinkData? { val r = sendCmd(null, CC.ApiStandaloneFileInfo(url), ctrl) - return if (r is CR.StandaloneFileInfo) { - r.fileMeta + return if (r is API.Result && r.res is CR.StandaloneFileInfo) { + r.res.fileMeta } else { Log.e(TAG, "standaloneFileInfo error: $r") null } } - 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 receiveFiles(rhId: Long?, user: UserLike, fileIds: List, userApprovedRelays: Boolean = false, auto: Boolean = false) { + val fileIdsToApprove = mutableListOf() + val srvsToApprove = mutableSetOf() + val otherFileErrs = mutableListOf() + + 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, + otherFileErrs: List, + 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) { @@ -1370,9 +1846,9 @@ object ChatController { suspend fun apiCancelFile(rh: Long?, fileId: Long, ctrl: ChatCtrl? = null): AChatItem? { val r = sendCmd(rh, CC.CancelFile(fileId), ctrl) - return when (r) { - is CR.SndFileCancelled -> r.chatItem_ - is CR.RcvFileCancelled -> r.chatItem_ + 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 @@ -1383,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)) @@ -1423,58 +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, withMessages: Boolean = false): List? { + 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, memberRole: GroupMemberRole): List { + 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 apiBlockMemberForAll(rh: Long?, groupId: Long, memberId: Long, blocked: Boolean): GroupMember = - when (val r = sendCmd(rh, CC.ApiBlockMemberForAll(groupId, memberId, blocked))) { - is CR.MemberBlockedForAllUser -> r.member - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiBlockMemberForAll", generalGetString(MR.strings.error_blocking_member_for_all), r) - } - throw Exception("failed to block member for all: ${r.responseType} ${r.details}") - } + suspend fun apiBlockMembersForAll(rh: Long?, groupId: Long, memberIds: List, blocked: Boolean): List { + 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 { 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 -> { @@ -1488,81 +1959,66 @@ object ChatController { } } - suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { - 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? { + 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? { - 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? { + 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? { - 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? { + 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) + } } } @@ -1570,7 +2026,7 @@ object ChatController { suspend fun listRemoteHosts(): List? { 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 } @@ -1583,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 } @@ -1612,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 { + suspend fun connectRemoteCtrl(desktopAddress: String): Pair { 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 { + suspend fun confirmRemoteCtrl(rcId: Long): Pair { 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? { 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 } @@ -1661,38 +2121,108 @@ object ChatController { private suspend fun sendCommandOkResp(rh: Long?, cmd: CC, ctrl: ChatCtrl? = null): Boolean { val r = sendCmd(rh, cmd, ctrl) - val ok = r is CR.CmdOk + 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 } @@ -1700,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.replaceConnReqView(conn.id, "@${r.contact.contactId}") - 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) { @@ -1734,43 +2314,69 @@ 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.replaceConnReqView(conn.id, "@${r.contact.contactId}") - 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, @@ -1780,7 +2386,9 @@ object ChatController { 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) { @@ -1800,137 +2408,265 @@ object ChatController { chatModel.networkStatuses[s.agentConnId] = s.networkStatus } } - is CR.NewChatItem -> withBGApi { - 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))) { - receiveFile(rhId, r.user, file.fileId, 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.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") - 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.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") - 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)) { - 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.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()) @@ -1938,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) @@ -1963,6 +2703,11 @@ object ChatController { 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) is CR.SndFileComplete -> { @@ -1989,6 +2734,11 @@ object ChatController { 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)) } @@ -1996,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") @@ -2031,21 +2781,45 @@ object ChatController { } } 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 @@ -2108,15 +2882,45 @@ object ChatController { val sess = chatModel.remoteCtrlSession.value if (sess != null) { chatModel.remoteCtrlSession.value = null + ModalManager.fullscreen.closeModals() fun showAlert(chatError: ChatError) { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.remote_ctrl_was_disconnected_title), - if (chatError is ChatError.ChatErrorRemoteCtrl) { - chatError.remoteCtrlError.localizedString - } else { - generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string) - } - ) + 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) @@ -2136,18 +2940,21 @@ object ChatController { } is CR.ContactPQEnabled -> if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) + } } - is CR.ChatRespError -> when { - r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> { - chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart) - } - r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.INTERNAL && appPrefs.developerTools.get() && appPrefs.showInternalErrors.get() -> { - chatModel.processedInternalError.newError(r.chatError.agentError, false) - } - } 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}") } } @@ -2196,29 +3003,101 @@ object ChatController { } } - suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) { - val encrypted = appPrefs.privacyEncryptLocalFiles.get() - 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() + } } } @@ -2258,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) { @@ -2289,19 +3176,18 @@ object ChatController { 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() @@ -2322,6 +3208,9 @@ object ChatController { hostMode = hostMode, requiredHostMode = requiredHostMode, sessionMode = sessionMode, + smpProxyMode = smpProxyMode, + smpProxyFallback = smpProxyFallback, + smpWebPortServers = smpWebPortServers, tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpTimeoutPerKb = tcpTimeoutPerKb, @@ -2333,13 +3222,16 @@ 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) @@ -2364,8 +3256,12 @@ class SharedPreference(val get: () -> T, set: (T) -> Unit) { 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()}") + } } } } @@ -2374,7 +3270,7 @@ class SharedPreference(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() @@ -2386,10 +3282,10 @@ sealed class CC { class ApiUnmuteUser(val userId: Long): CC() class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): 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() + @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() @@ -2398,43 +3294,65 @@ sealed class 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 ApiCreateChatItem(val noteFolderId: Long, val file: CryptoFile?, val mc: MsgContent): 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): CC() + class ApiCreateChatTag(val tag: ChatTagData): CC() + class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List): CC() + class ApiDeleteChatTag(val tagId: Long): CC() + class ApiUpdateChatTag(val tagId: Long, val tagData: ChatTagData): CC() + class ApiReorderChatTags(val tagIds: List): CC() + class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List): 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, val mode: CIDeleteMode): CC() + class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List): CC() + class ApiArchiveReceivedReports(val groupId: Long): CC() + class ApiDeleteReceivedReports(val groupId: Long, val itemIds: List, val mode: CIDeleteMode): CC() class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC() - class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long): 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): CC() + class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List, 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 ApiBlockMemberForAll(val groupId: Long, val memberId: Long, val blocked: Boolean): CC() - class ApiRemoveMember(val groupId: Long, val memberId: Long): CC() + class ApiMembersRole(val groupId: Long, val memberIds: List, val memberRole: GroupMemberRole): CC() + class ApiBlockMembersForAll(val groupId: Long, val memberIds: List, val blocked: Boolean): CC() + class ApiRemoveMembers(val groupId: Long, val memberIds: List, 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): 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): CC() + class ApiGetUserServers(val userId: Long): CC() + class ApiSetUserServers(val userId: Long, val userServers: List): CC() + class ApiValidateServers(val userId: Long, val userServers: List): CC() + class ApiGetUsageConditions(): CC() + class ApiSetConditionsNotified(val conditionsId: Long): CC() + class ApiAcceptConditions(val conditionsId: Long, val operatorIds: List): 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() @@ -2445,19 +3363,23 @@ 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() @@ -2473,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): 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() @@ -2498,12 +3421,15 @@ sealed class 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" @@ -2523,10 +3449,9 @@ sealed class CC { is ApiUnmuteUser -> "/_unmute user $userId" is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}" 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 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)}" @@ -2535,48 +3460,84 @@ sealed class CC { 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 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 ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId" - is ApiSendMessage -> { + 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 ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" + "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs" } - is ApiCreateChatItem -> { - "/_create *$noteFolderId json ${json.encodeToString(ComposedMessage(file, null, mc))}" + 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 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 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 ApiForwardChatItem -> "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId" + 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 ApiBlockMemberForAll -> "/_block #$groupId $memberId blocked=${onOff(blocked)}" - 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" @@ -2587,23 +3548,23 @@ 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)}" @@ -2619,11 +3580,13 @@ 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" @@ -2648,6 +3611,9 @@ sealed class CC { 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) { @@ -2665,10 +3631,9 @@ 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 ApiSetAppFilePaths -> "apiSetAppFilePaths" is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" @@ -2677,22 +3642,33 @@ sealed class CC { 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 ApiCreateChatItem -> "apiCreateChatItem" + 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 ApiForwardChatItem -> "apiForwardChatItem" + is ApiGetReactionMembers -> "apiGetReactionMembers" + is ApiForwardChatItems -> "apiForwardChatItems" + is ApiPlanForwardChatItems -> "apiPlanForwardChatItems" is ApiNewGroup -> "apiNewGroup" is ApiAddMember -> "apiAddMember" is ApiJoinGroup -> "apiJoinGroup" - is ApiMemberRole -> "apiMemberRole" - is ApiBlockMemberForAll -> "apiBlockMemberForAll" - is ApiRemoveMember -> "apiRemoveMember" + is ApiMembersRole -> "apiMembersRole" + is ApiBlockMembersForAll -> "apiBlockMembersForAll" + is ApiRemoveMembers -> "apiRemoveMembers" is ApiLeaveGroup -> "apiLeaveGroup" is ApiListMembers -> "apiListMembers" is ApiUpdateGroupProfile -> "apiUpdateGroupProfile" @@ -2702,18 +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" @@ -2726,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" @@ -2735,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" @@ -2753,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" @@ -2775,12 +3767,15 @@ sealed class CC { 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() } @@ -2804,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) = 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 ) @@ -2826,22 +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) + +@Serializable +class UpdatedMessage(val msgContent: MsgContent, val mentions: Map) { + 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) @@ -2856,24 +3861,311 @@ enum class ServerProtocol { } @Serializable -data class ProtoServersConfig( - val servers: List +enum class OperatorTag { + @SerialName("simplex") SimpleX, + @SerialName("flux") Flux +} + +data class ServerOperatorInfo( + val description: List, + val website: String, + val selfhost: Pair? = null, + val logo: ImageResource, + val largeLogo: ImageResource, + val logoDarkMode: ImageResource, + val largeLogoDarkMode: ImageResource +) +val operatorsInfo: Map = 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, - val presetServers: List, +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, val deadline: Instant?, val showNotice: Boolean) : UsageConditionsAction() + @Serializable @SerialName("accepted") data class Accepted(val operators: List) : UsageConditionsAction() + + val shouldShowNotice: Boolean + get() = when (this) { + is Review -> showNotice + else -> false + } +} + +@Serializable +data class ServerOperatorConditionsDetail( + val serverOperators: List, + 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, + 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, + val xftpServers: List +) { + 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() @@ -2887,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 ) ) } @@ -3025,59 +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 rcvConcurrency: Int, // pool size - val tcpKeepAlive: KeepAliveOpts?, + 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 = 25_000_000, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 10_000, rcvConcurrency = 12, - tcpKeepAlive = KeepAliveOpts.defaults, - smpPingInterval = 1200_000_000, - smpPingCount = 3 + smpPingInterval = 1200_000_000 ) val proxyDefaults: NetCfg = NetCfg( socksProxy = ":9050", - hostMode = HostMode.OnionViaSocks, - requiredHostMode = false, - sessionMode = TransportSessionMode.User, tcpConnectTimeout = 35_000_000, tcpTimeout = 20_000_000, tcpTimeoutPerKb = 15_000, rcvConcurrency = 8, - tcpKeepAlive = KeepAliveOpts.defaults, - smpPingInterval = 1200_000_000, - smpPingCount = 3 + smpPingInterval = 1200_000_000 ) } @@ -3098,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 } @@ -3107,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) } } @@ -3146,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 @@ -3227,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, + val previouslyUsedSMPServers: List, + val onlyProxiedSMPServers: List +) + +@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, + val previouslyUsedXFTPServers: List +) + +@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() @@ -3565,6 +5133,7 @@ enum class GroupFeature: Feature { @SerialName("voice") Voice, @SerialName("files") Files, @SerialName("simplexLinks") SimplexLinks, + @SerialName("reports") Reports, @SerialName("history") History; override val hasParam: Boolean get() = when(this) { @@ -3581,6 +5150,7 @@ enum class GroupFeature: Feature { Voice -> true Files -> true SimplexLinks -> true + Reports -> false History -> false } @@ -3593,6 +5163,7 @@ enum class GroupFeature: Feature { Voice -> generalGetString(MR.strings.voice_messages) Files -> generalGetString(MR.strings.files_and_media) SimplexLinks -> generalGetString(MR.strings.simplex_links) + Reports -> generalGetString(MR.strings.group_reports_member_reports) History -> generalGetString(MR.strings.recent_history) } @@ -3605,6 +5176,7 @@ enum class GroupFeature: Feature { 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) } @@ -3617,6 +5189,7 @@ enum class GroupFeature: Feature { 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) } @@ -3651,6 +5224,10 @@ enum class GroupFeature: Feature { 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) @@ -3664,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) @@ -3686,6 +5263,10 @@ enum class GroupFeature: Feature { 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) @@ -3805,6 +5386,7 @@ data class FullGroupPreferences( val voice: RoleGroupPreference, val files: RoleGroupPreference, val simplexLinks: RoleGroupPreference, + val reports: GroupPreference, val history: GroupPreference, ) { fun toGroupPreferences(): GroupPreferences = @@ -3816,7 +5398,8 @@ data class FullGroupPreferences( voice = voice, files = files, simplexLinks = simplexLinks, - history = history + reports = reports, + history = history, ) companion object { @@ -3828,6 +5411,7 @@ data class FullGroupPreferences( 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), ) } @@ -3842,6 +5426,7 @@ data class GroupPreferences( val voice: RoleGroupPreference? = null, val files: RoleGroupPreference? = null, val simplexLinks: RoleGroupPreference? = null, + val reports: GroupPreference? = null, val history: GroupPreference? = null, ) { companion object { @@ -3853,6 +5438,7 @@ data class GroupPreferences( 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), ) } @@ -3971,6 +5557,15 @@ 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 @@ -3981,58 +5576,126 @@ val jsonShort = Json { 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 = 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 { 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 { 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 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 { + override val descriptor: SerialDescriptor = buildSerialDescriptor("API", PolymorphicKind.SEALED) { + element("Result", buildClassSerialDescriptor("Result") { + element("remoteHostId") + element("result") + }) + element("Error", buildClassSerialDescriptor("Error") { + element("remoteHostId") + element("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(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(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 = 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) } } @@ -4064,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): 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): 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): CR() + @Serializable @SerialName("userServersValidation") class UserServersValidation(val user: UserRef, val serverErrors: List): 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() @@ -4082,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, val chatTags: List): 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() @@ -4102,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() @@ -4122,44 +5791,42 @@ sealed class CR { // TODO remove above @Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List): CR() @Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List): CR() - @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupRef): CR() - @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List): 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): CR() + @Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List): CR() @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR() @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): CR() + @Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List, val byUser: Boolean): CR() + @Serializable @SerialName("groupChatItemsDeleted") class GroupChatItemsDeleted(val user: UserRef, val groupInfo: GroupInfo, val chatItemIDs: List, val byUser: Boolean, val member_: GroupMember?): CR() + @Serializable @SerialName("forwardPlan") class ForwardPlan(val user: UserRef, val itemsCount: Int, val chatItemIds: List, 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, 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("membersRoleUser") class MembersRoleUser(val user: UserRef, val groupInfo: GroupInfo, val members: List, 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("memberBlockedForAllUser") class MemberBlockedForAllUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val blocked: Boolean): 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("membersBlockedForAllUser") class MembersBlockedForAllUser(val user: UserRef, val groupInfo: GroupInfo, val members: List, 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() @@ -4176,6 +5843,7 @@ sealed class 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("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() @@ -4188,7 +5856,8 @@ sealed class CR { @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() @Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List): 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): 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): CR() @@ -4197,6 +5866,7 @@ sealed class 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): CR() @Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR() @@ -4219,10 +5889,11 @@ sealed class CR { // misc @Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List, val agentMigrations: List): 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): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): 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() @@ -4235,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" @@ -4252,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" @@ -4272,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" @@ -4280,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" @@ -4290,40 +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 MemberBlockedForAllUser -> "memberBlockedForAllUser" + 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" @@ -4345,6 +6020,7 @@ sealed class CR { is RcvFileProgressXFTP -> "rcvFileProgressXFTP" is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP" is RcvFileError -> "rcvFileError" + is RcvFileWarning -> "rcvFileWarning" is SndFileStart -> "sndFileStart" is SndFileComplete -> "sndFileComplete" is SndFileRcvCancelled -> "sndFileRcvCancelled" @@ -4354,6 +6030,7 @@ sealed class CR { is SndStandaloneFileComplete -> "sndStandaloneFileComplete" is SndFileCancelledXFTP -> "sndFileCancelledXFTP" is SndFileError -> "sndFileError" + is SndFileWarning -> "sndFileWarning" is CallInvitations -> "callInvitations" is CallInvitation -> "callInvitation" is CallOffer -> "callOffer" @@ -4361,6 +6038,7 @@ sealed class CR { is CallExtraInfo -> "callExtraInfo" is CallEnded -> "callEnded" is ContactConnectionDeleted -> "contactConnectionDeleted" + is ContactDisabled -> "contactDisabled" is RemoteHostList -> "remoteHostList" is CurrentRemoteHost -> "currentRemoteHost" is RemoteHostStarted -> "remoteHostStarted" @@ -4378,9 +6056,10 @@ sealed class CR { 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" @@ -4394,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 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 UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}") 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)}") @@ -4412,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)) @@ -4432,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()) @@ -4450,43 +6135,41 @@ 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 MembersRoleUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\ntoRole: $toRole") is MemberBlockedForAll -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nblocked: $blocked") - is MemberBlockedForAllUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nblocked: $blocked") - is DeletedMemberUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member") - is DeletedMember -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember") + 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") @@ -4502,6 +6185,7 @@ sealed class CR { 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)) @@ -4513,7 +6197,8 @@ sealed class CR { is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem)) is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString()) is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_)) - is SndFileError -> 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)}") @@ -4521,6 +6206,7 @@ sealed class CR { 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_) @@ -4549,15 +6235,16 @@ 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 @@ -4569,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> = 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 @@ -4617,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 } @@ -4631,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) } } @@ -4652,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 @@ -4710,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 } @@ -4771,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 @@ -4798,6 +6536,7 @@ sealed class ChatErrorType { is ChatStoreChanged -> "chatStoreChanged" is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" + is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" is ContactNotReady -> "contactNotReady" is ContactNotActive -> "contactNotActive" @@ -4814,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" @@ -4824,15 +6562,14 @@ 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" @@ -4851,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" @@ -4877,6 +6615,7 @@ sealed class 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() @@ -4893,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() @@ -4910,8 +6648,7 @@ 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): 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() @@ -4930,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() @@ -4940,62 +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 ExtraFileDescrNotFoundXFTP -> "extraFileDescrNotFoundXFTP" - 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() @@ -5023,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() @@ -5030,24 +6788,27 @@ 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() @Serializable @SerialName("rcvFileNotFoundXFTP") class RcvFileNotFoundXFTP(val agentRcvFileId: String): StoreError() - @Serializable @SerialName("extraFileDescrNotFoundXFTP") class ExtraFileDescrNotFoundXFTP(val fileId: Long): StoreError() @Serializable @SerialName("connectionNotFound") class ConnectionNotFound(val agentConnId: String): StoreError() @Serializable @SerialName("connectionNotFoundById") class ConnectionNotFoundById(val connId: Long): StoreError() @Serializable @SerialName("connectionNotFoundByMemberId") class ConnectionNotFoundByMemberId(val groupMemberId: Long): StoreError() @Serializable @SerialName("pendingConnectionNotFound") class PendingConnectionNotFound(val connId: Long): StoreError() @Serializable @SerialName("introNotFound") object IntroNotFound: StoreError() @Serializable @SerialName("uniqueID") object UniqueID: StoreError() + @Serializable @SerialName("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() @@ -5055,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 @@ -5076,17 +6848,18 @@ 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}" @@ -5094,11 +6867,12 @@ sealed class AgentErrorType { 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() @@ -5157,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) { @@ -5201,12 +7016,14 @@ 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() @@ -5252,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" @@ -5267,6 +7085,7 @@ sealed class 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() @@ -5279,6 +7098,18 @@ sealed class 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) { @@ -5318,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 @@ -5404,15 +7235,26 @@ enum class NotificationsMode() { } } +@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, @@ -5425,17 +7267,30 @@ data class AppSettings( 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? = null, + var uiThemes: List? = 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 } @@ -5448,6 +7303,15 @@ data class AppSettings( 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 } @@ -5459,14 +7323,21 @@ data class AppSettings( 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) } @@ -5479,18 +7350,31 @@ data class AppSettings( 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, @@ -5502,7 +7386,16 @@ data class AppSettings( confirmDBUpgrades = false, androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW, iosCallKitEnabled = true, - iosCallKitCallsInRecents = false + 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 @@ -5510,12 +7403,16 @@ data class AppSettings( 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(), @@ -5528,6 +7425,15 @@ data class AppSettings( 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() ) } } @@ -5628,3 +7534,62 @@ enum class UserNetworkType { 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 = 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(private var capacity: Int) : LinkedList() { 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 0d447a4a5a..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,6 +1,7 @@ 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.* @@ -43,14 +44,13 @@ val appPreferences: AppPreferences val chatController: ChatController = ChatController -fun initChatControllerAndRunMigrations() { +fun initChatControllerOnStart() { withLongRunningApi { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { initChatController(startChat = ::showStartChatAfterRestartAlert) } else { initChatController() } - runMigrations() } } @@ -58,9 +58,16 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat 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 = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value) + var migrated: Array = 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(migrated[0] as String) }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } @@ -72,7 +79,11 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } if (rerunMigration) { chatModel.dbMigrationInProgress.value = true - migrated = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) + migrated = if (databaseBackend == "postgres") { + chatMigrateInit("simplex_v1", "postgresql://simplex@/simplex_v1", confirm.value) + } else { + chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) + } res = runCatching { json.decodeFromString(migrated[0] as String) }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } @@ -85,19 +96,46 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat 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.apiSetTempFolder(coreTmpDir.absolutePath) - controller.apiSetFilesFolder(appFilesDir.absolutePath) - if (appPlatform.isDesktop) { - controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) - } + 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 @@ -118,7 +156,11 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } else if (startChat().await()) { val savedOnboardingStage = appPreferences.onboardingStage.get() val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - OnboardingStage.Step3_CreateSimpleXAddress + if (appPlatform.isAndroid) { + OnboardingStage.Step4_SetNotificationsMode + } else { + OnboardingStage.OnboardingComplete + } } else { savedOnboardingStage } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index c788a6902e..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:")) @@ -78,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) { @@ -98,6 +121,49 @@ fun getLoadedFileSource(file: CIFile?): CryptoFile? { } } +fun readThemeOverrides(): List { + 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("themes") + val res = ArrayList() + 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): 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 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 4a10027746..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,27 +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) -> 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, showMenu: State,): 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 = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }, + scrollState: State, + 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 57c1e578ae..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) { @@ -65,7 +73,7 @@ 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) } } @@ -96,6 +104,7 @@ abstract class NtfManager { 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 Unit>> = emptyList()) abstract fun cancelCallNotification() abstract fun cancelAllNotifications() @@ -125,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 f61c5bc83e..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,14 +1,16 @@ package chat.simplex.common.platform -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector1D +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() {} @@ -20,15 +22,17 @@ interface PlatformInterface { 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 desktopScrollBarComponents(): Triple, Modifier, MutableState> = remember { Triple(Animatable(0f), Modifier, mutableStateOf(Job())) } - @Composable fun desktopScrollBar(state: LazyListState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) {} - @Composable fun desktopScrollBar(state: ScrollState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) {} + @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, sendMsgEnabled: Boolean, + disabledText: String?, sendMsgButtonDisabled: Boolean, textStyle: MutableState, showDeleteTextButton: MutableState, - userIsObserver: Boolean, - onMessageChange: (String) -> Unit, + placeholder: String, + showVoiceButton: Boolean, + onMessageChange: (ComposeMessage) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List) -> 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 1e902b5d88..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 fun play( fileSource: CryptoFile, audioPlaying: MutableState, progress: MutableState, duration: MutableState, resetOnEnd: Boolean, + smallView: Boolean, ) fun stop() fun stop(item: ChatItem) 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 index d4fa2fe125..b1965ec845 100644 --- 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 @@ -8,12 +8,13 @@ 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 = rememberLazyListState(), + state: LazyListState? = null, contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = @@ -21,6 +22,31 @@ expect fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, + additionalBarOffset: State? = null, + additionalTopBar: State = remember { mutableStateOf(false) }, + chatBottomBar: State = 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? = null, + additionalTopBar: State = remember { mutableStateOf(false) }, + chatBottomBar: State = remember { mutableStateOf(true) }, + maxHeight: State? = null, + containerAlignment: Alignment = Alignment.TopStart, content: LazyListScope.() -> Unit ) @@ -29,6 +55,22 @@ expect fun ColumnWithScrollBar( modifier: Modifier = Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, - state: ScrollState = rememberScrollState(), + 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/ui/theme/Color.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt index 5eeedbb2a0..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 @@ -31,17 +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 SentMessageColor = Color(0x1E45B8FF) val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black -val NoteFolderIconColor: Color @Composable get() = with(CurrentColors.collectAsState().value.appColors.sentMessage) { - // Default color looks too light and better to have it here a little bit brighter - if (alpha == SentMessageColor.alpha) { - copy(min(SentMessageColor.alpha + 0.1f, 1f)) - } else { - // Color is non-standard and theme maker can choose color without alpha at all since the theme bound to dark/light variant, - // and it shouldn't be universal - this - } -} - +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 62acc13bfe..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,58 +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.text.TextStyle -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 @@ -61,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 } } @@ -75,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) } } @@ -92,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 = 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(), @@ -139,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.getTheme(themeId: String?): ThemeOverrides? = + firstOrNull { it.themeId == themeId } -private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb()) +fun List.getTheme(themeId: String?, type: WallpaperType?, base: DefaultTheme): ThemeOverrides? = + firstOrNull { it.themeId == themeId || it.isSame(type, base.themeName)} + +fun List.replace(theme: ThemeOverrides): List { + 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.sameTheme(type: WallpaperType?, themeName: String): ThemeOverrides? = firstOrNull { it.isSame(type, themeName) } + +/** See [ThemesTest.testSkipDuplicates] */ +fun List.skipDuplicates(): List { + val res = ArrayList() + 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) @@ -164,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?, 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 @@ -204,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), @@ -212,8 +637,11 @@ val DarkColorPalette = darkColors( ) val DarkColorPaletteApp = AppColors( title = SimplexBlue, - sentMessage = SentMessageColor, - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFF18262E), + sentMessage = Color(0xFF18262E), + sentQuote = Color(0xFF1D3847), + receivedMessage = Color(0xff262627), + receivedQuote = Color(0xff373739), ) val LightColorPalette = lightColors( @@ -231,8 +659,11 @@ val LightColorPalette = lightColors( ) val LightColorPaletteApp = AppColors( title = SimplexBlue, - sentMessage = SentMessageColor, - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFFE9F7FF), + sentMessage = Color(0xFFE9F7FF), + sentQuote = Color(0xFFD6F0FF), + receivedMessage = Color(0xfff5f5f6), + receivedQuote = Color(0xffececee), ) val SimplexColorPalette = darkColors( @@ -251,11 +682,39 @@ val SimplexColorPalette = darkColors( ) val SimplexColorPaletteApp = AppColors( title = Color(0xFF267BE5), - sentMessage = SentMessageColor, - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFF172941), + sentMessage = Color(0xFF172941), + sentQuote = Color(0xFF1C3A57), + receivedMessage = Color(0xff25283a), + receivedQuote = Color(0xff36394a), ) -val CurrentColors: MutableStateFlow = 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 = MutableStateFlow(ThemeManager.currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())) @Composable fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight @@ -263,31 +722,115 @@ fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLi @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 = { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, 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 = 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? { + val nonSystemThemeName = nonSystemThemeName() + val defaultThemeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] + return appSettingsTheme.getTheme(defaultThemeId) + } + + fun defaultActiveTheme(perUserTheme: ThemeModeOverrides?, appSettingsTheme: List): 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): 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> { - val allThemes = ArrayList>() - 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> = 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) { + pref.value = pref.value.withUpdatedColor(name, color?.toReadableHex()) + } + + fun saveAndApplyWallpaper(baseTheme: DefaultTheme, type: WallpaperType?, pref: SharedPreference> = 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): 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) { + 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> = 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> = 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) { + 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 5265f3187b..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 @@ -6,11 +6,11 @@ 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( 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 1f49a98728..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, + 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,54 +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() } ) } - }, - contentColor = LocalContentColor.current, - drawerContentColor = LocalContentColor.current, - modifier = Modifier.navigationBarsWithImePadding() - ) { contentPadding -> - Surface( - modifier = Modifier - .padding(contentPadding) - .fillMaxWidth(), - color = MaterialTheme.colors.background, - contentColor = LocalContentColor.current - ) { - 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) { val reversedTerminalItems by remember { derivedStateOf { chatModel.terminalItems.value.asReversed() } } - val clipboard = LocalClipboardManager.current - LazyColumnWithScrollBar(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( @@ -143,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/*( @@ -170,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 41b30be640..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() } - ColumnWithScrollBar( - modifier = Modifier.fillMaxSize() - ) { + 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() } - - ColumnWithScrollBar( - modifier = Modifier.fillMaxSize() - ) { - /*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) @@ -180,7 +166,7 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { if (!chatModel.connectedToRemote()) { chatModel.localUserCreated.value = true } - controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators) controller.startChat(user) controller.switchUIRemoteHost(null) close() @@ -196,7 +182,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () 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() @@ -219,7 +205,7 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () 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, @@ -230,28 +216,6 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () } } -@Composable -fun OnboardingButtons(displayName: MutableState, 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, contentColor = LocalContentColor.current) { - 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, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) { var valid by rememberSaveable { mutableStateOf(true) } @@ -269,15 +233,12 @@ fun ProfileNameField(name: MutableState, 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, @@ -285,8 +246,32 @@ fun ProfileNameField(name: MutableState, 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 285658ec1d..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 @@ -43,13 +43,16 @@ class CallManager(val chatModel: ChatModel) { 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() @@ -77,6 +80,7 @@ class CallManager(val chatModel: ChatModel) { // 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() 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 e4a6691d49..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 @@ -10,15 +10,16 @@ import kotlinx.coroutines.* expect fun ActiveCallView() fun activeCallWaitDeliveryReceipt(scope: CoroutineScope) = scope.launch(Dispatchers.Default) { - for (apiResp in controller.messagesChannel) { + for (msg in controller.messagesChannel) { val call = chatModel.activeCall.value if (call == null || call.callState > CallState.InvitationSent) break - val msg = apiResp.resp - if (apiResp.remoteHostId == call.remoteHostId && - msg is CR.ChatItemStatusUpdated && - msg.chatItem.chatInfo.id == call.contact.id && - msg.chatItem.chatItem.content is CIContent.SndCall && - msg.chatItem.chatItem.meta.itemStatus is CIStatus.SndRcvd) { + 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 829a849ddc..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 @@ -53,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) { @@ -115,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 0a7231370b..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,10 +2,12 @@ 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 kotlin.collections.ArrayList @@ -13,19 +15,25 @@ 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, var localCamera: VideoCamera = VideoCamera.User, val connectionInfo: ConnectionInfo? = 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 -> "" @@ -34,10 +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 - - fun supportsVideo(): Boolean = peerMedia == CallMediaType.Video || localMedia == CallMediaType.Video - + val hasVideo: Boolean + get() = localMediaSources.hasVideo || peerMediaSources.hasVideo } enum class CallState { @@ -67,14 +73,25 @@ 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? = 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? = 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() @@ -89,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() @@ -105,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) { @@ -163,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, 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 dcd36e026b..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,38 +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?, @@ -51,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() @@ -64,6 +77,9 @@ 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, @@ -71,9 +87,19 @@ fun ChatInfoView( sendReceipts = sendReceipts, setSendReceipts = { sendRcpts -> val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) - updateChatSettings(chat, chatSettings, chatModel) + 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, customUserProfile, @@ -99,7 +125,9 @@ fun ChatInfoView( 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() } @@ -111,29 +139,23 @@ fun ChatInfoView( 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 = { withBGApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) - connStats.value = cStats - if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) - } + syncContactConnection(chatRh, contact, connStats, force = false) close.invoke() } }, syncContactConnectionForce = { showSyncConnectionForceAlert(syncConnectionForce = { withBGApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true) - connStats.value = cStats - if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) - } + syncContactConnection(chatRh, contact, connStats, force = true) close.invoke() } }) @@ -148,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 } }, @@ -163,11 +187,24 @@ fun ChatInfoView( ) } } - } + }, + close = close, + onSearchClicked = onSearchClicked, + deletingItems = deletingItems ) } } +suspend fun syncContactConnection(rhId: Long?, contact: Contact, connectionStats: MutableState, 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() @@ -198,34 +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() - 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() - 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() - 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({ @@ -238,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.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 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() @@ -282,7 +521,9 @@ fun ChatInfoLayout( currentUser: User, sendReceipts: State, setSendReceipts: (SendReceipts) -> Unit, - connStats: State, + chatItemTTL: MutableState, + setChatItemTTL: (ChatItemTTL?) -> Unit, + connStats: MutableState, contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, localAlias: String, @@ -297,6 +538,9 @@ fun ChatInfoLayout( syncContactConnection: () -> Unit, syncContactConnectionForce: () -> Unit, verifyClicked: () -> Unit, + close: () -> Unit, + onSearchClicked: () -> Unit, + deletingItems: State ) { val cStats = connStats.value val scrollState = rememberScrollState() @@ -304,10 +548,7 @@ fun ChatInfoLayout( KeyChangeEffect(chat.id) { scope.launch { scrollState.scrollTo(0) } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -316,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 { @@ -327,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) } @@ -337,12 +600,25 @@ 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() } + SectionDividerSpaced(maxBottomPadding = false) + + ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) val conn = contact.activeConn if (conn != null) { @@ -359,7 +635,7 @@ fun ChatInfoLayout( 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) { @@ -393,7 +669,7 @@ fun ChatInfoLayout( } } } - SectionDividerSpaced() + SectionDividerSpaced(maxBottomPadding = false) } SectionView { @@ -406,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() @@ -432,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( @@ -446,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) ) } } @@ -459,6 +755,7 @@ fun LocalAliasEditor( center: Boolean = true, leadingIcon: Boolean = false, focus: Boolean = false, + isContact: Boolean = true, updateValue: (String) -> Unit ) { val state = remember(chatId) { @@ -475,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 ) @@ -509,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 +) { + 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 +) { + 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, + 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( @@ -642,6 +1169,15 @@ private fun SendReceiptsOption(currentUser: User, state: State, 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( @@ -675,10 +1211,80 @@ fun ShareAddressButton(onClick: () -> Unit) { ) } +@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) + } } } @@ -711,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): 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, + previousChatTTL: ChatItemTTL?, + progressIndicator: MutableState +) { + 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, + progressIndicator: MutableState, + 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) { + 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() { @@ -725,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, @@ -740,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 df8e535f82..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 @@ -23,14 +23,16 @@ 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.platform.* -import chat.simplex.common.views.chat.item.ItemAction -import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.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): CIInfoTab() @@ -40,18 +42,20 @@ sealed class CIInfoTab { } @Composable -fun ChatItemInfoView(chatRh: Long?, 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.History) } @Composable - fun TextBubble(text: String, formattedText: List?, sender: String?, showMenu: MutableState) { + fun TextBubble(text: String, formattedText: List?, sender: String?, showMenu: MutableState, mentions: Map? = 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, @@ -73,12 +77,17 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools 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)) { @@ -120,7 +129,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools 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 } ) { @@ -200,7 +209,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools SectionItemView( click = { withBGApi { - openChat(chatRh, forwardedFromItem.chatInfo, chatModel) + openChat(secondaryChatsCtx = null, chatRh, forwardedFromItem.chatInfo) ModalManager.end.closeModals() } }, @@ -216,6 +225,27 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } + @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 (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message)) @@ -244,53 +274,56 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools 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() { - // LALAL SCROLLBAR DOESN'T WORK - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() 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() + SectionBottomSpacer() } } @Composable fun QuoteTab(qi: CIQuote) { - // LALAL SCROLLBAR DOESN'T WORK - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) - SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + 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) { - // LALAL SCROLLBAR DOESN'T WORK - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionView { @@ -300,15 +333,16 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools ForwardedFromView(forwardedFromItem) } SectionBottomSpacer() + SectionBottomSpacer() } } @Composable - fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) { + 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, @@ -317,7 +351,19 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools 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) { @@ -329,46 +375,37 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } 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) { - // LALAL SCROLLBAR DOESN'T WORK - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() 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() } } @@ -407,12 +444,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools Column { if (numTabs() > 1) { - Column( + Box( Modifier - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween + .fillMaxHeight() ) { - Column(Modifier.weight(1f)) { + Column { when (val sel = selection.value) { is CIInfoTab.Delivery -> { DeliveryTab(sel.memberDeliveryStatuses) @@ -452,28 +488,33 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses) } } - 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, - ) + 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 { @@ -482,10 +523,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } -private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List): List> { +private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List): List> { return memberDeliveryStatuses.mapNotNull { mds -> chatModel.getGroupMember(mds.groupMemberId)?.let { mem -> - mem to mds.memberDeliveryStatus + Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy) } } } @@ -519,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() + 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 = 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, + val newSplits: List, +) + +private fun removeDuplicatesAndModifySplitsOnBeforePagination( + unreadAfterItemId: StateFlow, + newItems: SnapshotStateList, + newIds: Set, + splits: StateFlow>, + 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() + 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, + newIds: Set, + chat: Chat, + splits: StateFlow> +): Pair, Int> { + var unreadInLoaded = unreadInLoaded + var firstItemIdBelowAllSplits: Long? = null + val splitsToRemove = ArrayList() + val indexInSplitRanges = splits.value.indexOf(paginationChatItemId) + // Currently, it should always load from split range + val loadingFromSplitRange = indexInSplitRanges != -1 + val topSplits: List + val splitsToMerge: ArrayList + 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 = 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, + chat: Chat, + splits: StateFlow>, + visibleItemIndexesNonReversed: () -> IntRange +): List { + 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>() + 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, + chat: Chat, + splits: List +): List { + 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): Pair, Int> { + var unreadInLoaded = 0 + val ids = mutableSetOf() + 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, 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, splits: Set): Pair { + 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 = 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 = 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, two: Pair) { + 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 = 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, + val splits: List, + // chat item id, index in list + val indexInParentItems: Map, +) { + companion object { + fun create(items: List, unreadCount: State, revealedItems: Set, chatState: ActiveChatState): MergedItems { + if (items.isEmpty()) return MergedItems(emptyList(), emptyList(), emptyMap()) + + val unreadAfterItemId = chatState.unreadAfterItemId + val itemSplits = chatState.splits.value + val mergedItems = ArrayList() + // Indexes of splits here will be related to reversedChatItems, not chatModel.chatItems + val splitRanges = ArrayList() + val indexInParentItems = mutableMapOf() + var index = 0 + var unclosedSplitIndex: Int? = null + var unclosedSplitIndexInParent: Int? = null + var visibleItemIndexInParent = -1 + var unreadBefore = unreadCount.value - chatState.unreadAfterNewestLoaded.value + var lastRevealedIdsInMergedItems: MutableList? = null + var lastRangeInReversedForMergedItems: MutableStateFlow? = 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, + 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, + val rangeInReversed: MutableStateFlow, + val mergeCategory: CIMergeCategory?, + val unreadIds: MutableSet, + override val startIndexInReversedItems: Int, + ): MergedItem() { + fun reveal(reveal: Boolean, revealedItems: MutableState>) { + 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> = MutableStateFlow(emptyList()), + val unreadAfterItemId: MutableStateFlow = MutableStateFlow(-1L), + // total items after unread after item (exclusive) + val totalAfter: MutableStateFlow = MutableStateFlow(0), + val unreadTotal: MutableStateFlow = MutableStateFlow(0), + // exclusive + val unreadAfter: MutableStateFlow = MutableStateFlow(0), + // exclusive + val unreadAfterNewestLoaded: MutableStateFlow = MutableStateFlow(0) +) { + fun moveUnreadAfterItem(toItemId: Long?, nonReversedItems: List) { + 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) { + 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?, newItems: List) { + 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) { + if (item.second) { + unreadAfter.value++ + unreadTotal.value++ + } + } + + fun itemsRemoved(itemIds: List>, newItems: List) { + val newSplits = ArrayList() + 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, 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 e6f90c1599..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,36 @@ 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.* @@ -38,493 +45,649 @@ 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, + scrollToItemId: MutableState = 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()) { - val draft = chatModel.draft.value - val sharedContent = chatModel.sharedContent.value - mutableStateOf( - if (chatModel.draftChatId.value == chatId && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == chatId)) { - draft - } else { - ComposeState(useLinkPreviews = useLinkPreviews) - } - ) - } - val attachmentOption = rememberSaveable { mutableStateOf(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() - .filterNotNull() - .collect { chatId -> - if (activeChat.value?.id != chatId) { - // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly - // Also for situation when chatId changes after clicking in notification, etc - activeChat.value = chatModel.getChat(chatId) - } - markUnreadChatAsRead(activeChat, chatModel) - } + 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() - // 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 - } - } - } - 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(null) } + val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val scope = rememberCoroutineScope() + val selectedChatItems = rememberSaveable { mutableStateOf(null as Set?) } + 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 - when (chat.chatInfo) { + CompositionLocalProvider( + LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false), + ) { + when (chatInfo) { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { - 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 + 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) } + + 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 + } + } + 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) + } + } + } + } + }, ) } - ComposeView( - chatModel, chat, composeState, attachmentOption, - showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } - ) - } - }, - attachmentOption, - attachmentBottomSheetState, - 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) - withBGApi { - // The idea is to preload information before showing a modal because large groups can take time to load all members - var preloadedContactInfo: Pair? = null - var preloadedCode: String? = null - var preloadedLink: Pair? = 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) + }, + 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 } - ModalManager.end.showModalCloseable(true) { close -> - val chat = remember { activeChat }.value - if (chat?.chatInfo is ChatInfo.Direct) { - var contactInfo: Pair? 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 + 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? = null + var preloadedCode: String? = null + var preloadedLink: Pair? = 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?> = 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? 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? 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 + } + } } - 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? 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) - withBGApi { - 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 + }, + showGroupReports = { + val info = activeChatInfo.value ?: return@ChatLayout + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout } - 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) + 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) + } } } - } - }, - loadPrevMessages = { - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout - val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) - val firstId = chatModel.chatItems.value.firstOrNull()?.id - if (c != null && firstId != null) { + }, + 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 { - apiLoadPrevMessages(c, chatModel, firstId, searchText.value) - } - } - }, - deleteMessage = { itemId, mode -> - withBGApi { - val cInfo = chat.chatInfo - val toDeleteItem = chatModel.chatItems.value.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 = 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) + 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) + } } - } - for (di in deletedItems) { - chatModel.removeChatItem(chatRh, chatInfo, di) - } - } - } - }, - 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 -> - withBGApi { - val cInfo = chat.chatInfo - if (cInfo is ChatInfo.Direct) { - val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) - val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi - chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) - chatModel.showCallView.value = true - chatModel.callCommand.add(WCallCommand.Capabilities(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 -> - withBGApi { - openDirectChat(chatRh, contactId, chatModel) - } - }, - updateContactStats = { contact -> - withBGApi { - 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 -> - withBGApi { - 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 -> - withBGApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) - if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) - } - } - }, - syncMemberConnection = { groupInfo, member -> - withBGApi { - 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 -> - withBGApi { - 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 -> - suspend fun loadChatItemInfo(): ChatItemInfo? { - 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) - } - } - return ciInfo - } - withBGApi { - var initialCiInfo = loadChatItemInfo() ?: return@withBGApi - 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()) - LaunchedEffect(cItem.id) { - withContext(Dispatchers.Default) { - for (apiResp in controller.messagesChannel) { - val msg = apiResp.resp - if (apiResp.remoteHostId == chatRh && - msg is CR.ChatItemStatusUpdated && - msg.chatItem.chatItem.id == cItem.id - ) { - ciInfo = loadChatItemInfo() ?: return@withContext - initialCiInfo = ciInfo + withContext(Dispatchers.Main) { + if (deletedChatItem.isReport) { + if (toChatItem != null) { + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem) } } } } - KeyChangeEffect(chatModel.chatId.value) { - close() + } + }, + 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) } } - } - }, - addMembers = { groupInfo -> - hideKeyboard(view) - withBGApi { - setGroupMembers(chatRh, groupInfo, chatModel) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - AddGroupMembersView(chatRh, groupInfo, false, chatModel, close) + }, + acceptFeature = { contact, feature, param -> + withBGApi { + chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param) } - } - }, - openGroupLink = { groupInfo -> - hideKeyboard(view) - withBGApi { - 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) + }, + openDirectChat = { contactId -> + scope.launch { + openDirectChat(chatRh, contactId) } - } - }, - 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 - withBGApi { - apiFindMessages(c, chatModel, value) - searchText.value = value - } - }, - onComposed, - developerTools = chatModel.controller.appPrefs.developerTools.get(), - ) + }, + 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 + ) + } } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } - ModalView(close, showClose = appPlatform.isAndroid, content = { - ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close) - }) - LaunchedEffect(chat.id) { - onComposed(chat.id) - ModalManager.end.closeModals() - chatModel.chatItems.clear() - } + 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() + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() + } + } } is ChatInfo.InvalidJSON -> { val close = { chatModel.chatId.value = null } - ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chat.chatInfo.json) } }, content = { - InvalidJSONView(chat.chatInfo.json) - }) - LaunchedEffect(chat.id) { - onComposed(chat.id) - ModalManager.end.closeModals() - chatModel.chatItems.clear() - } + 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() + } + } } 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, + chatInfo: State, unreadCount: State, composeState: MutableState, - composeView: (@Composable () -> Unit), + composeView: (@Composable (FocusRequester?) -> Unit), + scrollToItemId: MutableState, attachmentOption: MutableState, attachmentBottomSheetState: ModalBottomSheetState, searchValue: State, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, + selectedChatItems: MutableState?>, 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) -> Unit, + archiveReports: (List, Boolean) -> Unit, receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long, () -> Unit) -> Unit, @@ -533,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, @@ -543,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) -> Unit, + markItemsRead: (List) -> Unit, + markChatRead: () -> Unit, + changeNtfsState: (MsgFilter, currentValue: MutableState) -> Unit, onSearchValueChanged: (String) -> Unit, + closeSearch: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, + showViaProxy: Boolean, + showSearch: MutableState ) { val scope = rememberCoroutineScope() val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } @@ -555,59 +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() }, - contentColor = LocalContentColor.current, - drawerContentColor = LocalContentColor.current, - ) { contentPadding -> - BoxWithConstraints(Modifier - .fillMaxHeight() - .padding(contentPadding) - ) { - ChatItemsList( - chat, unreadCount, composeState, 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) + } } } } @@ -616,45 +876,48 @@ 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) -> Unit, + changeNtfsState: (MsgFilter, currentValue: MutableState) -> Unit, onSearchValueChanged: (String) -> Unit, + showSearch: MutableState ) { 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 } - if (chat.chatInfo is ChatInfo.Local) { + if (chatInfo is ChatInfo.Local) { barButtons.add { - IconButton({ - showMenu.value = false - showSearch = true - }, enabled = chat.chatInfo.noteFolder.ready + 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 (chat.chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } @@ -662,41 +925,27 @@ fun ChatInfoToolbar( menuItems.add { ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { showMenu.value = false - showSearch = true + showSearch.value = true }) } } - if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) { + 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 && appPlatform.isDesktop) { + } else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) { barButtons.add { val call = remember { chatModel.activeCall }.value val connectedAt = call?.connectedAt @@ -725,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) } @@ -754,25 +996,28 @@ 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) } } ) @@ -786,21 +1031,34 @@ fun ChatInfoToolbar( } } } - - DefaultTopAppBar( - navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } }, - title = { ChatInfoToolbarTitle(chat.chatInfo) }, - onTitleClick = if (chat.chatInfo is ChatInfo.Local) null else 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() } + } } } } @@ -812,9 +1070,9 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo 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 @@ -839,40 +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, composeState: MutableState, + composeViewHeight: State, searchValue: State, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, + scrollToItemId: MutableState, + selectedChatItems: MutableState?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, - loadPrevMessages: () -> Unit, + showChatInfo: () -> Unit, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, + archiveReports: (List, 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, @@ -881,333 +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) -> Unit, + markChatRead: () -> Unit, + closeSearch: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, + showViaProxy: Boolean ) { - val listState = rememberLazyListState() - val scope = rememberCoroutineScope() - ScrollToBottom(chat.id, listState, chatModel.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 { chatModel.chatItems.asReversed() } } - 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()) } + // 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() } - ) - LazyColumnWithScrollBar(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()) } + 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() } + 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, + showAvatar: Boolean, + cItem: ChatItem, + itemSeparation: ItemSeparation, + previousItemSeparationLargeGap: Boolean, + revealed: State, + 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) && chat.chatInfo !is ChatInfo.Local) { - 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, chatModel.chatItems.value, 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, fillMaxWidth: Boolean = true) { tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(chat.remoteHostId, 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 && cItem.meta.itemForwarded == 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), - maxLines = 2 - ) + fun ChatItemView(cItem: ChatItem, range: State, 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.clickable { showMemberInfo(chat.chatInfo.groupInfo, member) }) { - MemberImage(member) + @Composable + fun MemberNameAndRole(range: State) { + 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 + ) + } + } + } + + @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(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState) } +private suspend fun loadLastItems(chatsCtx: ChatModel.ChatsContext, chatId: State, listState: State, loadItems: State 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: State>) { - 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, reversedChatItems: State>) { 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.value.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)) { - if (appPlatform.isAndroid) listState.animateScrollToItem(0) else listState.scrollToItem(0) + if (listState.value.firstVisibleItemIndex == 0 || listState.value.firstVisibleItemIndex == 1) { + if (appPlatform.isAndroid) listState.value.animateScrollToItem(0) else listState.value.scrollToItem(0) } else { - if (appPlatform.isAndroid) listState.animateScrollBy(scrollDistance) else listState.scrollBy(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: State>, +private fun NotifyChatListOnFinishingComposition( + finishedInitialComposition: MutableState, + chatInfo: ChatInfo, + revealedItems: MutableState>, + listState: State, + 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>, + chatInfo: State, + topPaddingToContent: Dp, + topPaddingToContentPx: State, + loadingMoreItems: MutableState, + loadingTopItems: MutableState, + loadingBottomItems: MutableState, + animatedScrollingInProgress: MutableState, + mergedItems: State, unreadCount: State, - minUnreadItemId: Long, + maxHeight: State, + composeViewHeight: State, searchValue: State, - markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - setFloatingButton: (@Composable () -> Unit) -> Unit, - listState: LazyListState + markChatRead: () -> Unit, + listState: State, + 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 items = chatItems.value - val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems - if (items.size <= from || from < 0) return@derivedStateOf 0 - - items.subList(from, items.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.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), - bottomUnreadCount - ) + markChatRead() showDropDown.value = false }) } @@ -1216,126 +1872,725 @@ fun BoxWithConstraintsScope.FloatingButtons( @Composable fun PreloadItems( - listState: LazyListState, - remaining: Int = 10, - onLoadMore: () -> Unit, + chatsCtx: ChatModel.ChatsContext, + chatId: String, + ignoreLoadingRequests: MutableSet, + loadingMoreItems: State, + resetListState: State, + mergedItems: State, + listState: State, + 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, + ignoreLoadingRequests: State>, + mergedItems: State, + listState: State, + remaining: Int, + loadItems: State 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, + mergedItems: State, + remaining: Int, + loadItems: State 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, + requestedTopScroll: State, + animatedScrollingInProgress: State, 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, 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, + mergedItems: State, + listState: State, +) { + 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) { + 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, + finishedInitialComposition: State, + chatId: ChatId, + listState: State, + markItemsRead: (List) -> 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 = + chatsCtx.chatItems.value.asReversed() + +private fun oldestPartiallyVisibleListItemInListStateOrNull(topPaddingToContentPx: State, mergedItems: State, listState: State): 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, density: Float, fontSizeSqrtMultiplier: Float, mergedItems: State, listState: State): 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, + loadingMoreItems: MutableState, + animatedScrollingInProgress: MutableState, + highlightedItems: MutableState>, + chatInfo: State, + maxHeight: State, + scope: CoroutineScope, + reversedChatItems: State>, + mergedItems: State, + listState: State, + 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, + chatInfo: State, + 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, + showButtonWithCounter: State, + showButtonWithArrow: State, + requestedBottomScroll: State, + animatedScrollingInProgress: State, + composeViewHeight: State, + 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?>, +) { + 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, + selectedChatItems: MutableState?>, + reversedChatItems: State> +) { + val itemIds = mutableSetOf() + 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, 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, 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?>) { + 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 @@ -1345,9 +2600,11 @@ private fun markUnreadChatAsRead(activeChat: MutableState, 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) + } } } } @@ -1367,6 +2624,46 @@ private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount: } } +fun Modifier.chatViewBackgroundModifier( + colors: Colors, + wallpaper: AppWallpaper, + backgroundGraphicsLayerSize: MutableState?, + 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): 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 @@ -1374,14 +2671,23 @@ fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = null } +private suspend fun tryBlockAndSetLoadingMore(loadingMoreItems: MutableState, 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 fileSource: CryptoFile?, val preview: String): ProviderMedia() } -private fun providerForGallery( - listStateIndex: Int, +fun providerForGallery( chatItems: List, cItemId: Long, scrollTo: (Int) -> Unit @@ -1449,15 +2755,20 @@ private fun providerForGallery( 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 + +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() = @@ -1471,6 +2782,115 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf override val touchSlop: Float get() = slop } +private fun forwardContent(chatItemsIds: List, 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, @@ -1500,26 +2920,27 @@ 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(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), searchValue, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, + selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, + showGroupReports = {}, showMemberInfo = { _, _ -> }, - loadPrevMessages = {}, + loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, + archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, @@ -1528,6 +2949,7 @@ fun PreviewChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1538,11 +2960,15 @@ fun PreviewChatLayout() { showItemDetails = { _, _ -> }, addMembers = { _ -> }, openGroupLink = {}, - markRead = { _, _ -> }, + markItemsRead = { _ -> }, + markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, + closeSearch = {}, onComposed = {}, developerTools = false, + showViaProxy = false, + showSearch = remember { mutableStateOf(false) } ) } } @@ -1572,26 +2998,27 @@ 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(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), searchValue, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, + selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, + showGroupReports = {}, showMemberInfo = { _, _ -> }, - loadPrevMessages = {}, + loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = {}, + archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, @@ -1600,6 +3027,7 @@ fun PreviewGroupChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1610,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 26ff8796d4..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,4 +1,4 @@ -@file:UseSerializers(UriSerializer::class) +@file:UseSerializers(UriSerializer::class, ComposeMessageSerializer::class) package chat.simplex.common.views.chat import androidx.compose.foundation.background @@ -11,12 +11,17 @@ 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 @@ -28,10 +33,15 @@ 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() @@ -46,7 +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 ForwardingItem(val chatItem: ChatItem, val fromChatInfo: ChatInfo): ComposeContextItem() + @Serializable class ForwardingItems(val chatItems: List, val fromChatInfo: ChatInfo): ComposeContextItem() + @Serializable class ReportedItem(val chatItem: ChatItem, val reason: ReportReason): ComposeContextItem() } @Serializable @@ -57,23 +68,57 @@ data class LiveMessage( val sent: Boolean ) +typealias MentionedMembers = Map + +@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 { + 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 = 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 + 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) { @@ -82,7 +127,22 @@ data class ComposeState( } val forwarding: Boolean get() = when (contextItem) { - is ComposeContextItem.ForwardingItem -> true + 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 @@ -91,12 +151,12 @@ data class ComposeState( is ComposePreview.MediaPreview -> true is ComposePreview.VoicePreview -> true is ComposePreview.FilePreview -> true - else -> message.isNotEmpty() || forwarding || 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() = @@ -115,7 +175,7 @@ data class ComposeState( val attachmentDisabled: Boolean get() { - if (editing || forwarding || liveMessage != null || inProgress) return true + if (editing || forwarding || liveMessage != null || inProgress || reporting) return true return when (preview) { ComposePreview.NoPreview -> false is ComposePreview.CLinkPreview -> false @@ -132,8 +192,14 @@ data class ComposeState( 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, *> = Saver( @@ -143,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) @@ -166,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 } } @@ -195,7 +274,7 @@ fun MutableState.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( @@ -248,7 +327,7 @@ suspend fun MutableState.processPickedMedia(uris: List, 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)) } } @@ -258,15 +337,15 @@ fun ComposeView( chat: Chat, composeState: MutableState, attachmentOption: MutableState, - showChooseAttachment: () -> Unit + showChooseAttachment: () -> Unit, + focusRequester: FocusRequester?, ) { val cancelledLinks = rememberSaveable { mutableSetOf() } fun isSimplexLink(link: String): Boolean = link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) - fun parseMessage(msg: String): Pair { - if (msg.isBlank()) return null to false - val parsedMsg = parseToMarkdown(msg) ?: return null to false + fun getSimplexLink(parsedMsg: List?): Pair { + if (parsedMsg == null) return null to false val link = parsedMsg.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) } val simplexLink = parsedMsg.any { ft -> ft.format is Format.SimplexLink } return link?.text to simplexLink @@ -274,7 +353,7 @@ fun ComposeView( val linkUrl = rememberSaveable { mutableStateOf(null) } // default value parsed because of draft - val hasSimplexLink = rememberSaveable { mutableStateOf(parseMessage(composeState.value.message).second) } + val hasSimplexLink = rememberSaveable { mutableStateOf(getSimplexLink(parseToMarkdown(composeState.value.message.text)).second) } val prevLinkUrl = rememberSaveable { mutableStateOf(null) } val pendingLinkUrl = rememberSaveable { mutableStateOf(null) } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() @@ -282,7 +361,6 @@ fun ComposeView( val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } val recState: MutableState = remember { mutableStateOf(RecordingState.NotStarted) } - AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } } fun loadLinkPreview(url: String, wait: Long? = null) { @@ -302,11 +380,11 @@ fun ComposeView( } } - fun showLinkPreview(s: String) { + fun showLinkPreview(parsedMessage: List?) { prevLinkUrl.value = linkUrl.value - val parsed = parseMessage(s) - linkUrl.value = parsed.first - hasSimplexLink.value = parsed.second + 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) { @@ -375,58 +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): ChatItem? { val cInfo = chat.chatInfo - val aChatItem = if (chat.chatInfo.chatType == ChatType.Local) - chatModel.controller.apiCreateChatItem(rh = chat.remoteHostId, noteFolderId = chat.chatInfo.apiId, file = file, mc = mc) + 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.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 + 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? { val cInfo = chat.chatInfo val cs = composeState.value - var sent: ChatItem? - val msgText = text ?: cs.message + var sent: List? + 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: ChatItem, fromChatInfo: ChatInfo): ChatItem? { - val chatItem = controller.apiForwardChatItem( + suspend fun forwardItem(rhId: Long?, forwardedItem: List, fromChatInfo: ChatInfo, ttl: Int?): List? { + val chatItems = controller.apiForwardChatItems( rh = rhId, toChatType = chat.chatInfo.chatType, toChatId = chat.chatInfo.apiId, fromChatType = fromChatInfo.chatType, fromChatId = fromChatInfo.apiId, - itemId = forwardedItem.id + itemIds = forwardedItem.map { it.id }, + ttl = ttl ) - if (chatItem != null) { - chatModel.addChatItem(rhId, chat.chatInfo, chatItem) + + withContext(Dispatchers.Main) { + chatItems?.forEach { chatItem -> + chatModel.chatsContext.addChatItem(rhId, chat.chatInfo, chatItem) + } } - return 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).first + val parsedMsg = parseToMarkdown(msgText) + val url = getSimplexLink(parsedMsg).first val lp = composePreview.linkPreview if (lp != null && url == lp.uri) { MsgContent.MCLink(msgText, preview = lp) @@ -439,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() @@ -447,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? { + 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) + } } } @@ -468,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 @@ -489,16 +620,31 @@ fun ComposeView( if (chat.nextSendGrpInv) { sendMemberContactInvitation() sent = null - } else if (cs.contextItem is ComposeContextItem.ForwardingItem) { - sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo) - if (cs.message.isNotEmpty()) { - sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = null) + } 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) } - } else if (cs.contextItem is ComposeContextItem.EditingItem) { + 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 = ArrayList() val files: ArrayList = ArrayList() @@ -507,6 +653,7 @@ 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 file = when (it) { is UploadContent.SimpleImage -> @@ -516,7 +663,7 @@ fun ComposeView( if (remoteHost == null) saveAnimImage(it.uri) else CryptoFile.desktopPlain(it.uri) is UploadContent.Video -> - if (remoteHost == null) saveFileFromUri(it.uri) + if (remoteHost == null) saveFileFromUri(it.uri, hiddenFileNamePrefix = "video") else CryptoFile.desktopPlain(it.uri) } if (file != null) { @@ -590,22 +737,27 @@ 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) + } } } val wasForwarding = cs.forwarding - val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItem)?.fromChatInfo?.id - clearState(live) + 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 @@ -621,21 +773,22 @@ fun ComposeView( } } - 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) + if (s.text.isNotEmpty()) { + showLinkPreview(parsedMessage) } else { resetLinkPreview() hasSimplexLink.value = false } - } else if (s.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)) { - hasSimplexLink.value = parseMessage(s).second + } else if (s.text.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)) { + hasSimplexLink.value = getSimplexLink(parsedMessage).second } else { hasSimplexLink.value = false } @@ -703,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) @@ -724,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.value.findLast { it.meta.editable } + val lastEditable = chatModel.chatsContext.chatItems.value.findLast { it.meta.editable } if (lastEditable != null) { composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) } @@ -775,25 +928,47 @@ fun ComposeView( @Composable fun MsgNotAllowedView(reason: String, icon: Painter) { - val color = CurrentColors.collectAsState().value.appColors.receivedMessage - Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) { + 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.ForwardingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_forward), showSender = false) { + 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) } } @@ -812,11 +987,11 @@ 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.ForwardingItem(shared.chatItem, shared.fromChatInfo), + contextItem = ComposeContextItem.ForwardingItems(shared.chatItems, shared.fromChatInfo), preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview ) null -> {} @@ -824,15 +999,18 @@ fun ComposeView( 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) @@ -860,153 +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(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) + 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 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() + } + 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 + } + } + } + } + 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, ) } - 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 - } - } - 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.userCanSend).value) { - if (!chat.userCanSend) { - clearCurrentDraft() - clearState() - } - } - - 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() - } - 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 - } - } - } - } - - 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, - sendButtonEnabled = sendMsgEnabled.value && !(simplexLinkProhibited || fileProhibited || voiceProhibited), - 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 = 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 - ) } } } 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 502074d629..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,7 +12,6 @@ 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.* @@ -20,7 +19,9 @@ 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( @@ -40,8 +41,10 @@ fun ContactPreferencesView( 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() } @@ -78,10 +81,7 @@ private fun ContactPreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.contact_preferences)) val timedMessages: MutableState = 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 = 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 = 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 = 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 = 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 ce34ecf0c3..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 @@ -12,29 +12,34 @@ import androidx.compose.desktop.ui.tooling.preview.Preview 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, contextIcon: Painter, showSender: Boolean = true, - cancelContextItem: () -> Unit + 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 MessageText(attachment: ImageResource?, lines: Int) { + fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int) { val inlineContent: Pair Unit, Map>? = if (attachment != null) { remember(contextItem.id) { val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = { @@ -59,25 +64,34 @@ fun ContextItemView( 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(): ImageResource? = - when (contextItem.content.msgContent) { - is MsgContent.MCFile -> MR.images.ic_draft_filled + 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 -> MR.images.ic_play_arrow_filled + is MsgContent.MCVoice -> if (fileIsLoaded) MR.images.ic_play_arrow_filled else null else -> null } + } @Composable - fun ContextMsgPreview(lines: Int) { - MessageText(remember(contextItem.id) { attachment() }, lines) + 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 ) { @@ -95,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 (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(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 { - ContextMsgPreview(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) { @@ -129,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 73017c3d42..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,22 +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() - ) { +fun ScanCodeView(verifyCode: suspend (String?) -> Boolean, close: () -> Unit) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.scan_code)) QRCodeScanner { text -> - verifyCode(text) { - if (it) { - close() - } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.incorrect_code) - ) - } + 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), 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?>, 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?>, + 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, + selectedChatItems: MutableState?>, + deleteEnabled: MutableState, + deleteForEveryoneEnabled: MutableState, + canArchiveReports: MutableState, + canModerate: MutableState, + moderateEnabled: MutableState, + forwardEnabled: MutableState, + deleteCountProhibited: MutableState, + forwardCountProhibited: MutableState +) { + 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() + 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 58705bd00a..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,28 +39,29 @@ fun SendMsgView( isDirectChat: Boolean, liveMessageAlertShown: SharedPreference, sendMsgEnabled: Boolean, + userCantSendReason: Pair?, sendButtonEnabled: Boolean, nextSendGrpInv: Boolean, needToAllowVoiceToContact: Boolean, allowedVoiceByPrefs: Boolean, - userIsObserver: Boolean, - userCanSend: Boolean, sendButtonColor: Color = MaterialTheme.colors.primary, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, customDisappearingMessageTimePref: SharedPreference? = null, + placeholder: String, sendMessage: (Int?) -> Unit, sendLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null, editPrevMessage: () -> Unit, onFilesPasted: (List) -> Unit, - onMessageChange: (String) -> Unit, - textStyle: MutableState -) { + onMessageChange: (ComposeMessage) -> Unit, + textStyle: MutableState, + focusRequester: FocusRequester? = null, + ) { val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } - - 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) { @@ -71,35 +72,55 @@ fun SendMsgView( false } } - val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && - !composeState.value.forwarding && 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 || !sendButtonEnabled - PlatformTextField(composeState, sendMsgEnabled, sendMsgButtonDisabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) { + 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() @@ -111,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) } } @@ -134,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) } @@ -143,7 +167,7 @@ fun SendMsgView( } } } - cs.liveMessage?.sent == false && cs.message.isEmpty() -> { + cs.liveMessage?.sent == false && cs.message.text.isEmpty() -> { CancelLiveMessageButton { cancelLiveMessage?.invoke() } @@ -157,7 +181,7 @@ fun SendMsgView( fun MenuItems(): List<@Composable () -> Unit> { val menuItems = mutableListOf<@Composable () -> Unit>() - if (cs.liveMessage == null && !cs.editing && !cs.forwarding && !nextSendGrpInv || sendMsgEnabled) { + if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) { if ( cs.preview !is ComposePreview.VoicePreview && cs.contextItem is ComposeContextItem.NoContextItem && @@ -263,7 +287,7 @@ private fun CustomDisappearingMessageDialog( @Composable private fun BoxScope.DeleteTextButton(composeState: MutableState) { 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) @@ -323,8 +347,8 @@ private fun RecordVoiceView(recState: MutableState, 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), @@ -410,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( @@ -418,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 @@ -439,23 +464,23 @@ private fun SendMsgButton( } @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) @@ -554,19 +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, ) } } @@ -589,19 +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, ) } } @@ -624,19 +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 5bd707ab66..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 @@ -35,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 } } ) @@ -54,13 +54,9 @@ private fun VerifyCodeLayout( displayName: String, connectionCode: String, connectionVerified: Boolean, - verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, + verifyCode: suspend (String?) -> Boolean, ) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .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) { @@ -104,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) { @@ -115,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 d546b51a93..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) { @@ -58,7 +61,12 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea 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 } @@ -78,15 +86,16 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea fun getContactsToAdd(chatModel: ChatModel, search: String): List { 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() .map { it.contact } - .filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) } + .filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s) + } .sortedBy { it.displayName.lowercase() } .toList() } @@ -121,14 +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) } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.button_add_members)) profileText() Spacer(Modifier.size(DEFAULT_PADDING)) @@ -166,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) } @@ -204,8 +212,8 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState 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, @@ -251,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() } @@ -259,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, ) } } @@ -314,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 c52dd941fd..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,39 +19,62 @@ 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?) -> Unit, close: () -> Unit) { +fun ModalData.GroupChatInfoView( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chatId: String, + groupLink: CreatedConnLink?, + groupLinkMemberRole: GroupMemberRole?, + selectedItems: MutableState?>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, + scrollToItemId: MutableState, + onGroupLinkUpdated: (Pair?) -> 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, @@ -57,17 +82,33 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi sendReceipts = sendReceipts, setSendReceipts = { sendRcpts -> val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) - updateChatSettings(chat, chatSettings, chatModel) + 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 } .sortedByDescending { it.memberRole }, developerTools, + onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) }, groupLink, + selectedItems, + appBar, + scrollToItemId, addMembers = { - withBGApi { + scope.launch(Dispatchers.Default) { setGroupMembers(rhId, groupInfo, chatModel) + if (!isActive) return@launch + ModalManager.end.showModalCloseable(true) { close -> AddGroupMembersView(rhId, groupInfo, false, chatModel, close) } @@ -114,31 +155,41 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi 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 = { 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() } } }, @@ -147,9 +198,14 @@ 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 = { withLongRunningApi(60_000) { @@ -162,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 = { - withBGApi { - 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, 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, setSendReceipts: (SendReceipts) -> Unit, - members: List, + chatItemTTL: MutableState, + setChatItemTTL: (ChatItemTTL?) -> Unit, + activeSortedMembers: List, developerTools: Boolean, - groupLink: String?, + onLocalAliasChanged: (String) -> Unit, + groupLink: CreatedConnLink?, + selectedItems: MutableState?>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, + scrollToItemId: MutableState, addMembers: () -> Unit, showMemberInfo: (GroupMember) -> Unit, editGroupProfile: () -> Unit, @@ -197,73 +342,181 @@ fun GroupChatInfoLayout( clearChat: () -> Unit, leaveGroup: () -> Unit, manageGroupLink: () -> Unit, + close: () -> Unit = { ModalManager.closeAllModalsEverywhere()}, + onSearchClicked: () -> Unit, + deletingItems: State ) { - 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()) } } } - // LALAL strange scrolling + 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( - Modifier - .fillMaxWidth(), - state = listState + 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 { @@ -271,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) } } @@ -288,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?>, activeMembers: State>) { + 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?>, + filteredMembers: State>, + 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 = 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, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State) { + 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 ) } @@ -357,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 @@ -368,30 +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.Observer)) { + 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) { @@ -399,22 +785,37 @@ 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 + ) + } } } - MemberInfo() + if (infoPage) { + MemberInfo() + } + if (selected) { + Icon( + painterResource( + MR.images.ic_check + ), + null, + Modifier.size(20.dp), + tint = MaterialTheme.colors.primary, + ) + } } } @@ -424,8 +825,8 @@ private fun MemberVerifiedShield() { } @Composable -private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState) { - if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { +private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, selectedItems: MutableState?>, showMenu: MutableState) { + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { val canBlockForAll = member.canBlockForAll(groupInfo) val canRemove = member.canBeRemoved(groupInfo) if (canBlockForAll || canRemove) { @@ -449,6 +850,10 @@ private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: G showMenu.value = false }) } + if (selectedItems.value == null && member.memberRole < GroupMemberRole.Moderator) { + Divider() + SelectItemAction(showMenu) { toggleItemSelection(member.groupMemberId, selectedItems) } + } } } } else if (!member.blockedByAdmin) { @@ -514,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 @@ -525,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 @@ -548,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, 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 toggleItemSelection(itemId: T, selectedItems: MutableState?>) { + 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, @@ -562,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 5291520566..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,11 +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.ColumnWithScrollBar -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 @@ -28,13 +28,13 @@ fun GroupLinkView( chatModel: ChatModel, rhId: Long?, groupInfo: GroupInfo, - connReqContact: String?, + connLinkContact: CreatedConnLink?, memberRole: GroupMemberRole?, - onGroupLinkUpdated: ((Pair?) -> Unit)?, + onGroupLinkUpdated: ((Pair?) -> 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() { @@ -100,7 +100,7 @@ fun GroupLinkView( @Composable fun GroupLinkLayout( - groupLink: String?, + groupLink: CreatedConnLink?, groupInfo: GroupInfo, groupLinkMemberRole: MutableState, creatingLink: Boolean, @@ -119,9 +119,7 @@ fun GroupLinkLayout( ) } - ColumnWithScrollBar( - Modifier, - ) { + 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) + 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 e90efa7d1b..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, @@ -67,30 +86,42 @@ fun GroupMemberInfoView( getContactChat = { chatModel.getContactChat(it) }, openDirectChat = { withBGApi { - val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it) - if (c != null) { - if (chatModel.getContactChat(it) == null) { - chatModel.addChat(c) - } - chatModel.chatItemStatuses.clear() - chatModel.chatItems.replaceAll(c.chatItems) - chatModel.chatId.value = c.id + apiLoadMessages(chatModel.chatsContext, rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) + if (chatModel.getContactChat(it) != null) { closeAll() } } }, createMemberContact = { - 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()) - 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 -> @@ -105,17 +136,10 @@ fun GroupMemberInfoView( 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 }) { - withBGApi { - 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 = { @@ -124,7 +148,12 @@ fun GroupMemberInfoView( 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() } } @@ -136,21 +165,19 @@ fun GroupMemberInfoView( 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 = { - withBGApi { - 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 = { @@ -158,7 +185,12 @@ fun GroupMemberInfoView( 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() } } @@ -174,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 } }, @@ -200,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 = { withBGApi { - val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId) - if (removedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, removedMember) + 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() } @@ -219,6 +266,7 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c @Composable fun GroupMemberInfoLayout( + rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connStats: MutableState, @@ -242,17 +290,17 @@ fun GroupMemberInfoLayout( verifyClicked: () -> Unit, ) { val cStats = connStats.value - fun knownDirectChat(contactId: Long): Chat? { + fun knownDirectChat(contactId: Long): Pair? { 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 } } @Composable - fun AdminDestructiveSection() { + fun ModeratorDestructiveSection() { val canBlockForAll = member.canBlockForAll(groupInfo) val canRemove = member.canBeRemoved(groupInfo) if (canBlockForAll || canRemove) { @@ -291,10 +339,7 @@ fun GroupMemberInfoLayout( } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -305,17 +350,69 @@ fun GroupMemberInfoLayout( val contactId = member.memberContactId - if (member.memberActive) { - SectionView { - if (contactId != null && knownDirectChat(contactId) != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) + 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 = 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(onClick = { openDirectChat(contactId) }) - } else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) { - OpenChatButton(onClick = { createMemberContact() }) + 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 (connectionCode != null) { VerifyCodeButton(member.verified, verifyClicked) } @@ -347,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 ) } @@ -386,8 +477,8 @@ fun GroupMemberInfoLayout( } } - if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { - AdminDestructiveSection() + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ModeratorDestructiveSection() } else { NonAdminBlockSection() } @@ -397,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 = 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") @@ -423,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( @@ -437,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) ) } } @@ -495,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 ) } @@ -539,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, 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, @@ -558,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) withBGApi { - planAndConnect(rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) + planAndConnect(rhId, connReqUri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) } } catch (e: RuntimeException) { AlertManager.shared.showAlertMsg( @@ -604,7 +797,12 @@ 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)) + } } } } @@ -615,7 +813,19 @@ fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName), confirmText = generalGetString(MR.strings.block_for_all), onConfirm = { - blockMemberForAll(rhId, gInfo, mem, true) + blockMemberForAll(rhId, gInfo, listOf(mem.groupMemberId), true) + }, + destructive = true, + ) +} + +fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List, 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, ) @@ -627,15 +837,36 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName), confirmText = generalGetString(MR.strings.unblock_for_all), onConfirm = { - blockMemberForAll(rhId, gInfo, mem, false) + blockMemberForAll(rhId, gInfo, listOf(mem.groupMemberId), false) }, ) } -fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { +fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List, 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, blocked: Boolean, onSuccess: () -> Unit = {}) { withBGApi { - val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) - chatModel.upsertGroupMember(rhId, gInfo, updatedMember) + 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() } } @@ -644,6 +875,7 @@ fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocke fun PreviewGroupMemberInfoLayout() { SimpleXTheme { GroupMemberInfoLayout( + rhId = null, groupInfo = GroupInfo.sampleData, member = GroupMember.sampleData, connStats = remember { mutableStateOf(null) }, 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?>, + activeMembers: State>, + 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?>, + activeMembers: List, + deleteEnabled: MutableState, + blockForAllEnabled: MutableState, + unblockForAllEnabled: MutableState, + roleToMemberEnabled: MutableState, + roleToObserverEnabled: MutableState, +) { + val selected = selectedItems.value ?: return + var rDeleteEnabled = true + var rBlockForAllEnabled = true + var rUnblockForAllEnabled = true + var rRoleToMemberEnabled = true + var rRoleToObserverEnabled = true + val rSelectedItems = mutableSetOf() + 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, + 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(null) } + val mentionMemberId = remember { mutableStateOf(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) { + 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? { + 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, + range: TextRange +): Pair? { + 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, parsedMsg: List) { + 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 265d0cdeae..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,23 +6,23 @@ import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView -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 dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.* private val featureRoles: List> = 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) ) @@ -43,8 +43,13 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> 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() } @@ -79,10 +84,9 @@ private fun GroupPreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { - 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))) @@ -123,20 +127,24 @@ private fun GroupPreferencesLayout( applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role))) } - // TODO enable simplexLinks preference in 5.8 -// 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 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.canEdit) { + if (groupInfo.isOwner) { SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) ResetSaveButtons( reset = reset, @@ -163,12 +171,13 @@ private fun FeatureSection( 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, enableForRole?.value) } @@ -189,8 +198,6 @@ private fun FeatureSection( generalGetString(MR.strings.feature_enabled_for), featureRoles, enableForRole, - // remove in v5.8 - enabled = remember { mutableStateOf(false) }, onSelected = { value -> onSelected(enableFeature.value, value) } @@ -216,7 +223,7 @@ private fun FeatureSection( onSelected(enableFeature.value, null) } } - SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit)) + 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 7975a298d1..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 @@ -38,7 +37,9 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl 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,9 +95,7 @@ fun GroupProfileLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { ModalView(close = closeWithAlert) { - ColumnWithScrollBar( - Modifier - ) { + ColumnWithScrollBar { Column( Modifier.fillMaxWidth() .padding(horizontal = DEFAULT_PADDING) @@ -174,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, scrollToItemId: MutableState) { + 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, scrollToItemId: MutableState, 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 3bbeceb03c..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 @@ -7,9 +7,7 @@ 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 @@ -29,11 +27,9 @@ 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.ColumnWithScrollBar -import chat.simplex.common.platform.chatJsonLength -import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.platform.* import chat.simplex.res.MR -import kotlinx.coroutines.delay +import kotlinx.coroutines.* private const val maxByteCount = 1200 @@ -52,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() @@ -92,13 +90,11 @@ private fun GroupWelcomeLayout( linkMode: SimplexLinkMode, save: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + 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( @@ -130,13 +126,7 @@ private fun GroupWelcomeLayout( val clipboard = LocalClipboardManager.current CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) } - Divider( - Modifier.padding( - start = DEFAULT_PADDING_HALF, - top = 8.dp, - end = DEFAULT_PADDING_HALF, - bottom = 8.dp) - ) + SectionDividerSpaced(maxBottomPadding = false) SaveButton( save = save, 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 5d3d5aa94a..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 @@ -20,6 +20,7 @@ fun CICallItemView( cItem: ChatItem, status: CICallStatus, duration: Int, + showTimestamp: Boolean, acceptCall: (Contact) -> Unit, timedMessagesTTL: Int? ) { @@ -47,7 +48,7 @@ fun CICallItemView( CICallStatus.Error -> {} } - CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false) + 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 19cc949543..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,15 +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, + revealed: State, showMenu: MutableState, ) { - val merged = if (!revealed.value) mergedFeatures(chatItem, chatInfo) else emptyList() + val merged = if (!revealed.value) mergedFeatures(chatsCtx, chatItem, chatInfo) else emptyList() Box( Modifier .combinedClickable( @@ -71,13 +72,12 @@ private fun Feature.toFeatureInfo(color: Color, param: Int?, type: String): Feat ) @Composable -private fun mergedFeatures(chatItem: ChatItem, chatInfo: ChatInfo): List? { - val m = ChatModel +private fun mergedFeatures(chatsCtx: ChatModel.ChatsContext, chatItem: ChatItem, chatInfo: ChatInfo): List? { val fs: ArrayList = arrayListOf() val icons: MutableSet = 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], chatInfo) ?: break if (!icons.contains(f.icon)) { 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 6ad75057f6..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, + showMenu: MutableState, + 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,8 +58,9 @@ 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 ) } @@ -62,7 +70,7 @@ fun CIFileView( fun fileAction() { if (file != null) { when { - file.fileStatus is CIFileStatus.RcvInvitation -> { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> { if (fileSizeValid(file)) { receiveFile(file.fileId) } else { @@ -86,7 +94,15 @@ fun CIFileView( ) FileProtocol.LOCAL -> {} } - file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> { + 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) { @@ -107,71 +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)), - contentColor = LocalContentColor.current - ) { - 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(file)) - fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary) + 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 { @@ -180,38 +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.clickable(onClick = { fileAction() }).padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), - //Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.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, + encrypted: Boolean, + ext: String?, + encryptedUri: URI, + fileSource: CryptoFile, + saveFile: () -> Unit +) + @Composable fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher = rememberFileChooserLauncher(false, ciFile) { to: URI? -> @@ -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 { 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 e3008f36b3..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 @@ -26,6 +26,7 @@ fun CIGroupInvitationView( ci: ChatItem, groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole, + showTimestamp: Boolean, chatIncognito: Boolean = false, joinGroup: (Long, () -> Unit) -> Unit, timedMessagesTTL: Int? @@ -83,8 +84,8 @@ fun CIGroupInvitationView( } } - 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 @@ -110,6 +111,7 @@ 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) { @@ -117,7 +119,7 @@ fun CIGroupInvitationView( Text( 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)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) } }, color = if (inProgress.value) MaterialTheme.colors.secondary @@ -128,7 +130,7 @@ fun CIGroupInvitationView( Text( buildAnnotatedString { append(groupInvitationStr()) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) } } ) } @@ -144,7 +146,7 @@ fun CIGroupInvitationView( } } - CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false) + CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp) } } } @@ -161,7 +163,8 @@ fun PendingCIGroupInvitationViewPreview() { groupInvitation = CIGroupInvitation.getSample(), memberRole = GroupMemberRole.Admin, joinGroup = { _, _ -> }, - timedMessagesTTL = null + timedMessagesTTL = null, + showTimestamp = true, ) } } @@ -178,8 +181,9 @@ fun CIGroupInvitationViewAcceptedPreview() { groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted), memberRole = GroupMemberRole.Admin, joinGroup = { _, _ -> }, - timedMessagesTTL = null - ) + timedMessagesTTL = null, + showTimestamp = true, + ) } } @@ -195,7 +199,8 @@ fun CIGroupInvitationViewLongNamePreview() { ), memberRole = GroupMemberRole.Admin, joinGroup = { _, _ -> }, - timedMessagesTTL = null - ) + 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 65fb38575d..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,11 +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.* 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 @@ -17,25 +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?, - metaColor: Color, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, + smallView: Boolean, receiveFile: (Long) -> Unit ) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } @Composable fun progressIndicator() { CircularProgressIndicator( @@ -51,7 +53,7 @@ fun CIImageView( icon, stringResource(stringId), Modifier.fillMaxSize(), - tint = metaColor + tint = Color.White ) } @@ -60,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 ) { @@ -75,24 +77,21 @@ fun CIImageView( 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( @@ -106,8 +105,9 @@ 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, ) } @@ -117,7 +117,7 @@ fun CIImageView( // 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.width <= 4320 && painter.intrinsicSize.height <= 4320) { + if (painter.intrinsicSize != Size.Unspecified && painter.intrinsicSize.width <= 4320 && painter.intrinsicSize.height <= 4320) { Image( painter, contentDescription = stringResource(MR.strings.image_descr), @@ -129,17 +129,19 @@ 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, ) } else { Box(Modifier - .width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) + .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 = {} ) - .onRightClick { showMenu.value = true }, + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), contentAlignment = Alignment.Center ) { imageView(base64ToBitmap(image), onClick = { @@ -175,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?> = remember { @@ -193,7 +196,7 @@ fun CIImageView( } } else { KeyChangeEffect(file) { - if (res.value == null) { + if (res.value == null || res.value!!.third != getLoadedFilePath(file)) { res.value = imageAndFilePath(file) } } @@ -201,12 +204,12 @@ fun CIImageView( val loaded = res.value if (loaded != null && file != null) { val (imageBitmap, data, _) = loaded - SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, 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) } else { @@ -215,7 +218,7 @@ fun CIImageView( 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( @@ -229,23 +232,48 @@ fun CIImageView( ) 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/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index b40d8989e1..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 @@ -35,7 +34,9 @@ fun CIMetaView( blue = minOf(metaColor.red * 1.33F, 1F)) }, showStatus: Boolean = true, - showEdited: Boolean = true + showEdited: Boolean = true, + showTimestamp: Boolean, + showViaProxy: Boolean, ) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { if (chatItem.isDeletedContent) { @@ -53,7 +54,9 @@ fun CIMetaView( metaColor, paleMetaColor, showStatus = showStatus, - showEdited = showEdited + showEdited = showEdited, + showViaProxy = showViaProxy, + showTimestamp = showTimestamp ) } } @@ -68,11 +71,12 @@ private fun CIMetaText( color: Color, paleColor: Color, showStatus: Boolean = true, - showEdited: 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) @@ -80,9 +84,13 @@ private fun CIMetaText( if (ttl != chatTTL) { Text(shortTimeText(ttl), color = color, fontSize = 12.sp) } + } + 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 @@ -91,38 +99,79 @@ private fun CIMetaText( } else { StatusIconText(painterResource(icon), statusColor) } - 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?, showStatus: Boolean = true, showEdited: Boolean = true): String { - val iconSpace = " " - var res = "" - if (showEdited && 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 (showStatus && (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 @@ -137,7 +186,9 @@ fun PreviewCIMetaView() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -149,7 +200,9 @@ fun PreviewCIMetaViewUnread() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.RcvNew() ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -159,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 ) } @@ -172,7 +227,9 @@ fun PreviewCIMetaViewSendNoAuth() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth() ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -183,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 ) } @@ -195,7 +254,9 @@ fun PreviewCIMetaViewEdited() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = true ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -208,7 +269,9 @@ fun PreviewCIMetaViewEditedUnread() { itemEdited = true, status= CIStatus.RcvNew() ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -221,7 +284,9 @@ fun PreviewCIMetaViewEditedSent() { itemEdited = true, status= CIStatus.SndSent(SndCIStatusProgress.Complete) ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -230,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 318a8a6a05..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,7 +138,7 @@ 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), @@ -164,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) } } } @@ -184,7 +186,7 @@ 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), @@ -195,14 +197,15 @@ fun DecryptionErrorItem( 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 similarity index 62% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt index a79e509d02..8289149ad9 100644 --- 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 @@ -19,7 +19,9 @@ 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 @@ -31,14 +33,18 @@ fun CIVideoView( file: CIFile?, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, + smallView: Boolean = false, receiveFile: (Long) -> Unit ) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } Box( - Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), + 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) { @@ -63,17 +69,21 @@ fun CIVideoView( 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) { - VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen) + 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, openFullscreen = openFullscreen) + VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen) } } else { Box { - VideoPreviewImageView(preview, onClick = { + VideoPreviewImageView(preview, blurred = blurred, onClick = { if (file != null) { when (file.fileStatus) { - CIFileStatus.RcvInvitation -> + CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> receiveFileIfValidSize(file, receiveFile) CIFileStatus.RcvAccepted -> when (file.fileProtocol) { @@ -96,18 +106,26 @@ fun CIVideoView( } } }, + smallView = smallView, onLongClick = { showMenu.value = true }) - if (file != null) { + if (file != null && !smallView) { DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) } - if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } + if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) { + PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } } } } - loadingIndicator(file) + // 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) + } + } } } @@ -119,16 +137,17 @@ private fun VideoViewEncrypted( defaultDuration: Long, autoPlay: MutableState, showMenu: MutableState, + blurred: MutableState, openFullscreen: () -> Unit, ) { var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) } val onLongClick = { showMenu.value = true } Box { - VideoPreviewImageView(defaultPreview, if (decryptionInProgress) {{}} else openFullscreen, onLongClick) + VideoPreviewImageView(defaultPreview, smallView = false, blurred = blurred, if (decryptionInProgress) {{}} else openFullscreen, onLongClick) if (decryptionInProgress) { - VideoDecryptionProgress(onLongClick = onLongClick) - } else { - PlayButton(false, onLongClick = onLongClick) { + VideoDecryptionProgress(1f, onLongClick = onLongClick) + } else if (!blurred.value) { + PlayButton(false, 1f, onLongClick = onLongClick) { decryptionInProgress = true withBGApi { try { @@ -145,7 +164,82 @@ private fun VideoViewEncrypted( } @Composable -private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, autoPlay: MutableState, showMenu: MutableState, openFullscreen: () -> Unit) { +private fun SmallVideoViewEncrypted( + uriUnencrypted: MutableState, + file: CIFile, + defaultPreview: ImageBitmap, + autoPlay: MutableState, + showMenu: MutableState, + 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, + 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, + showMenu: MutableState, + blurred: MutableState, + 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 } @@ -186,9 +280,9 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau stop ) if (showPreview.value) { - VideoPreviewImageView(preview, openFullscreen, onLongClick) - if (!autoPlay.value) { - PlayButton(brokenVideo, onLongClick = onLongClick, play) + 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*/) @@ -199,16 +293,16 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau 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) { +private fun BoxScope.PlayButton(error: Boolean = false, sizeMultiplier: Float = 1f, onLongClick: () -> Unit, onClick: () -> Unit) { Surface( - Modifier.align(Alignment.Center), + 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 = 40.dp, minHeight = 40.dp) + .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 @@ -216,6 +310,7 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, 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 ) } @@ -223,25 +318,25 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, } @Composable -fun BoxScope.VideoDecryptionProgress(onLongClick: () -> Unit) { +fun BoxScope.VideoDecryptionProgress(sizeMultiplier: Float = 1f, onLongClick: () -> Unit) { Surface( - Modifier.align(Alignment.Center), + 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 = 40.dp, minHeight = 40.dp) + .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(30.dp), + .size(if (sizeMultiplier != 1f) 30.sp.toDp() * sizeMultiplier else 30.sp.toDp()), color = Color.White, - strokeWidth = 2.5.dp + strokeWidth = 2.5.sp.toDp() * sizeMultiplier ) } } @@ -293,7 +388,13 @@ private fun DurationProgress(file: CIFile, playing: MutableState, durat } @Composable -fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) { +fun VideoPreviewImageView( + preview: ImageBitmap, + smallView: Boolean, + blurred: MutableState, + 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( @@ -305,8 +406,9 @@ fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick onLongClick = onLongClick, onClick = onClick ) - .onRightClick(onLongClick), - contentScale = ContentScale.FillWidth, + .onRightClick(onLongClick) + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = onLongClick), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, ) } @@ -329,6 +431,9 @@ fun VideoPreviewImageViewFullScreen(preview: ImageBitmap, onClick: () -> Unit, o @Composable expect fun LocalWindowWidth(): Dp +@Composable +expect fun LocalWindowHeight(): Dp + @Composable private fun progressIndicator() { CircularProgressIndicator( @@ -339,11 +444,13 @@ private fun progressIndicator() { } @Composable -private fun fileIcon(icon: Painter, stringId: StringResource) { +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.fillMaxSize(), + modifier, tint = Color.White ) } @@ -364,11 +471,11 @@ private fun progressCircle(progress: Long, total: Long) { } @Composable -private fun loadingIndicator(file: CIFile?) { +private fun fileStatusIcon(file: CIFile?, smallView: Boolean) { if (file != null) { Box( Modifier - .padding(8.dp) + .padding(if (smallView) 0.dp else 8.dp) .size(20.dp), contentAlignment = Alignment.Center ) { @@ -387,7 +494,22 @@ private fun loadingIndicator(file: CIFile?) { } 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.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 -> @@ -396,15 +518,34 @@ private fun loadingIndicator(file: CIFile?) { } 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) + 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) - else -> {} } } } } +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) 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 e59c5f1370..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) -> 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,6 +120,9 @@ private fun VoiceLayout( sent: Boolean, hasText: Boolean, timedMessagesTTL: Int?, + showViaProxy: Boolean, + showTimestamp: Boolean, + sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, @@ -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, padding: PaddingValues) { +private fun DurationText(text: State, 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, 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,12 +247,14 @@ 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, @@ -230,19 +263,103 @@ private fun PlayPauseButton( ) { 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, + duration: State, + 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 ) } } @@ -257,44 +374,73 @@ private fun VoiceMsgIndicator( progress: State?, duration: State?, error: Boolean, + sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, longClick: () -> 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) }, {}, 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) } } @@ -320,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 10078dc266..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,18 +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 = @@ -39,8 +54,15 @@ 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, @@ -48,17 +70,26 @@ fun ChatItemView( imageProvider: (() -> ImageGalleryProvider)? = null, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, - revealed: MutableState, - range: IntRange?, + revealed: State, + highlighted: State, + hoveredItemId: MutableState, + range: State, + selectedChatItems: MutableState?>, + searchIsNotBlank: State, + fillMaxWidth: Boolean = true, + selectChatItem: () -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, + archiveReports: (List, 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, @@ -67,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 @@ -75,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()) } + val interactionSource = remember { MutableInteractionSource() } + val enterInteraction = remember { HoverInteraction.Enter() } + KeyChangeEffect(highlighted.value) { + if (highlighted.value) { + interactionSource.emit(enterInteraction) + } else { + interactionSource.emit(HoverInteraction.Exit(enterInteraction)) } } + + 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() + + 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( @@ -121,421 +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, 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 DeleteItemMenu() { - DefaultDropdownMenu(showMenu) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - } - - @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.content.msgContent != null && cItem.id >= 0 -> { - 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 } - fun fileForwardingAllowed() = when { - cItem.file != null && chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded -> true - getLoadedFilePath(cItem.file) != null -> true - else -> false - } - val copyAndShareAllowed = when { - cItem.content.text.isNotEmpty() -> true - fileForwardingAllowed() -> 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 || fileForwardingAllowed()) && - !cItem.isLiveDummy && !live - ) { - ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { - chatModel.chatId.value = null - chatModel.sharedContent.value = SharedContent.Forward(cItem, cInfo) - 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 && !cItem.localNote) { - 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) - } - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - } - else -> { - DefaultDropdownMenu(showMenu) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - } - } - } - - @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.itemForwarded == 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 LegacyDeletedItem() { - 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, cInfo.timedMessagesTTL) - DeleteItemMenu() - } - - 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 DeletedItem() { - 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) - } - } - - @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, 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, timedMessagesTTL = cInfo.timedMessagesTTL) - DeleteItemMenu() - } - is CIContent.SndGroupInvitation -> { - CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, 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(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.SndChatFeature -> { - CIChatFeatureView(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(cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvGroupFeature -> { - CIChatFeatureView(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(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(cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvGroupFeatureRejected -> { - CIChatFeatureView(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() - } + } 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) { + 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): 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): 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() } @@ -587,31 +829,38 @@ fun ItemInfoAction( @Composable fun DeleteItemAction( + chatsCtx: ChatModel.ChatsContext, cItem: ChatItem, - revealed: MutableState, + revealed: State, showMenu: MutableState, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> 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) { - val currIndex = chatModel.getChatItemIndexOrNull(cItem) + 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 = arrayListOf() - val reversedChatItems = chatModel.chatItems.asReversed() for (i in range) { 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) } @@ -645,53 +894,165 @@ fun ModerateItemAction( } @Composable -private fun RevealItemAction(revealed: MutableState, showMenu: MutableState) { +fun SelectItemAction( + showMenu: MutableState, + 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, showMenu: MutableState, 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, showMenu: MutableState) { +private fun HideItemAction(revealed: State, showMenu: MutableState, 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, showMenu: MutableState) { +private fun ExpandItemAction(revealed: State, showMenu: MutableState, 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, showMenu: MutableState) { +private fun ShrinkItemAction(revealed: State, showMenu: MutableState, 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, + showMenu: MutableState, +) { + 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, archiveReports: (List, 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, allowForAll: Boolean, archiveReports: (List, 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) { @@ -712,6 +1073,60 @@ 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) { @@ -749,6 +1164,160 @@ fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Un } } +@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), @@ -776,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.deletable && !chatItem.localNote) { + if (chatItem.meta.deletable && !chatItem.localNote && !chatItem.isReport) { Spacer(Modifier.padding(horizontal = 4.dp)) TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast) @@ -788,7 +1357,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes ) } -fun deleteMessagesAlertDialog(itemIds: List, questionText: String, deleteMessages: (List) -> Unit) { +fun deleteMessagesAlertDialog(itemIds: List, questionText: String, forAll: Boolean, deleteMessages: (List, Boolean) -> Unit) { AlertManager.shared.showAlertDialogButtons( title = generalGetString(MR.strings.delete_messages__question).format(itemIds.size), text = questionText, @@ -800,14 +1369,29 @@ fun deleteMessagesAlertDialog(itemIds: List, 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), @@ -820,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, questionText: String, deleteMessages: (List) -> 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) } ) } @@ -831,39 +1418,53 @@ expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) @Preview @Composable -fun PreviewChatItemView() { - SimpleXTheme { - ChatItemView( - rhId = null, - 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 @@ -871,6 +1472,7 @@ fun PreviewChatItemView() { fun PreviewChatItemViewDeletedContent() { SimpleXTheme { ChatItemView( + chatsCtx = ChatModel.ChatsContext(contentTag = null), rhId = null, ChatInfo.Direct.sampleData, ChatItem.getDeletedContentSampleData(), @@ -878,16 +1480,24 @@ fun PreviewChatItemViewDeletedContent() { 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 = { _ -> }, + archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, acceptCall = { _ -> }, scrollToItem = {}, + scrollToQuotedItemFromItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -896,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 7514b6e280..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,10 +16,10 @@ 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, @@ -36,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) } } } @@ -50,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 2ac97321c6..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, + 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,10 +49,7 @@ 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, @@ -59,14 +58,19 @@ fun FramedItemView( 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()) @@ -76,24 +80,24 @@ 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, pad: Boolean = false) { - 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 (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -104,7 +108,7 @@ fun FramedItemView( icon, caption, Modifier.size(18.dp), - tint = if (isInDarkTheme()) FileDark else FileLight + tint = iconColor ?: if (isInDarkTheme()) FileDark else FileLight ) } Text( @@ -122,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 -> { @@ -179,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.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 @@ -201,30 +200,75 @@ 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)) - } - 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.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) } - } else if (ci.meta.isLive) { - FramedItemHeader(stringResource(MR.strings.live), false) } if (ci.quotedItem != null) { - ciQuoteView(ci.quotedItem) - } else if (ci.meta.itemForwarded != null) { - FramedItemHeader(ci.meta.itemForwarded.text(chatInfo.chatType), true, painterResource(MR.images.ic_forward), pad = true) + 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) + } } if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { @@ -241,47 +285,61 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, 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) } } } @@ -290,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, 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( @@ -319,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 @@ -341,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) @@ -354,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 = 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, + 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 = 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 { 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 1dd5e4ee69..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 @@ -14,9 +14,9 @@ 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 chat.simplex.res.MR import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import java.net.URI @@ -40,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() } 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() @@ -174,7 +191,7 @@ private fun VideoViewEncrypted(uriUnencrypted: MutableState, fileSource: C } Box(contentAlignment = Alignment.Center) { VideoPreviewImageViewFullScreen(defaultPreview, {}, {}) - VideoDecryptionProgress {} + VideoDecryptionProgress() {} } } 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 7be0cc2f6c..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,8 +49,8 @@ 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), @@ -69,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) } } } @@ -84,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 0e2e8867cb..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,9 +20,9 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState) { - 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, 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, @@ -33,19 +33,19 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl 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) { - var i = getChatItemIndexOrNull(chatItem) +private fun MergedMarkedDeletedText(chatsCtx: ChatModel.ChatsContext, chatItem: ChatItem, chatInfo: ChatInfo, revealed: State) { + 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 @@ -67,7 +67,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState - 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) is CIDeleted.BlockedByAdmin -> @@ -112,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 66061767e5..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,8 @@ fun MarkdownText ( sender: String? = null, meta: CIMeta? = null, chatTTL: Int? = null, + mentions: Map? = null, + userMemberId: String? = null, toggleSecrets: Boolean, style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp), maxLines: Int = Int.MAX_VALUE, @@ -69,7 +69,10 @@ fun MarkdownText ( modifier: Modifier = Modifier, linkMode: SimplexLinkMode, inlineContent: Pair Unit, Map>? = null, - onLinkLongClick: (link: String) -> Unit = {} + 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 @@ -77,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 { " " } @@ -121,6 +124,7 @@ fun MarkdownText ( 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) { @@ -134,6 +138,7 @@ fun MarkdownText ( val annotatedText = buildAnnotatedString { inlineContent?.first?.invoke(this) appendSender(this, sender, senderBold) + if (prefix != null) append(prefix) for ((i, ft) in formattedText.withIndex()) { if (ft.format == null) append(ft.text) else if (toggleSecrets && ft.format is Format.Secret) { @@ -143,6 +148,26 @@ fun MarkdownText ( 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) { @@ -286,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/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 2324d62ea3..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,22 +11,25 @@ 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.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 @Composable @@ -47,6 +48,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { 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) @@ -56,16 +58,19 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { } } + 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, disabled, 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) @@ -77,14 +82,15 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { 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, disabled, 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) @@ -95,14 +101,16 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { 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) + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) } }, - click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, + click = defaultClickAction, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { NoteFolderMenuItems(chat, showMenu, showMarkRead) @@ -121,7 +129,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { 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) @@ -172,76 +180,80 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { } @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? = null) { +suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = 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) } } -fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) { - withBGApi { openChat(rhId, ChatInfo.Local(noteFolder), 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 openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) { - val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId) - if (chat != null) { - openLoadedChat(chat, chatModel) +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) { - val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId) - if (chat != null) { - openLoadedChat(chat, chatModel) - } -} - -fun openLoadedChat(chat: Chat, chatModel: ChatModel) { - chatModel.chatItemStatuses.clear() - chatModel.chatItems.replaceAll(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.replaceAll(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 @@ -252,20 +264,23 @@ 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, 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) + ToggleNotificationsChatAction(chat, chatModel, contact.chatSettings.enableNtfs.nextMode(false), showMenu) + TagListAction(chat, showMenu) ClearChatAction(chat, showMenu) } DeleteContactAction(chat, chatModel, showMenu) @@ -299,12 +314,18 @@ 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) + 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) @@ -319,7 +340,7 @@ fun GroupMenuItems( @Composable fun NoteFolderMenuItems(chat: Chat, showMenu: MutableState, showMarkRead: Boolean) { if (showMarkRead) { - MarkReadChatAction(chat, chatModel, showMenu) + MarkReadChatAction(chat, showMenu) } else { MarkUnreadChatAction(chat, chatModel, showMenu) } @@ -327,12 +348,12 @@ fun NoteFolderMenuItems(chat: Chat, showMenu: MutableState, showMarkRea } @Composable -fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { +fun MarkReadChatAction(chat: Chat, showMenu: MutableState) { ItemAction( stringResource(MR.strings.mark_read), painterResource(MR.images.ic_check), onClick = { - markChatRead(chat, chatModel) + markChatRead(chat) ntfManager.cancelNotificationsForChat(chat.id) showMenu.value = false } @@ -352,24 +373,46 @@ fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableStat } @Composable -fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState) { +fun TagListAction( + chat: Chat, + showMenu: MutableState +) { + val userTags = remember { chatModel.userTags } 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), + stringResource(if (chat.chatInfo.chatTags.isNullOrEmpty()) MR.strings.add_to_list else MR.strings.change_list), + painterResource(MR.images.ic_label), onClick = { - toggleChatFavorite(chat, !favorite, chatModel) + 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 ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState) { +fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState) { 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), + 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 = { - toggleNotifications(chat, !ntfsEnabled, chatModel) + toggleChatFavorite(chat.remoteHostId, chat.chatInfo, !favorite, chatModel) + showMenu.value = false + } + ) +} + +@Composable +fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, nextMsgFilter: MsgFilter, showMenu: MutableState) { + ItemAction( + generalGetString(nextMsgFilter.text(chat.chatInfo.hasMentions)), + painterResource(nextMsgFilter.icon), + onClick = { + toggleNotifications(chat.remoteHostId, chat.chatInfo, nextMsgFilter, chatModel) showMenu.value = false } ) @@ -467,13 +510,13 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh } @Composable -fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState) { +fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState, 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 } ) @@ -482,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 } ) @@ -506,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 }, @@ -547,17 +590,32 @@ private fun InvalidDataView() { } } -fun markChatRead(c: Chat, chatModel: ChatModel) { +@Composable +private fun ArchiveAllReportsItemAction(showMenu: MutableState, 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 } @@ -569,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) + } } } } @@ -580,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, @@ -587,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)), @@ -600,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) } @@ -622,12 +687,16 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque ) } -fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) { +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) } } } @@ -635,7 +704,9 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { withBGApi { chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) - chatModel.removeChat(rhId, contactRequest.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, contactRequest.id) + } } } @@ -651,7 +722,9 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti 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() } } @@ -670,7 +743,9 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo 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() @@ -700,7 +775,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) if (ok && openChat) { - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } }) { @@ -712,7 +787,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) if (ok && openChat) { - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } }) { @@ -732,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), @@ -773,7 +850,9 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { 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() @@ -791,22 +870,22 @@ fun groupInvitationAcceptedAlert(rhId: Long?) { ) } -fun toggleNotifications(chat: Chat, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState? = 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? = 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? = null) { - val newChatInfo = when(chat.chatInfo) { - is ChatInfo.Direct -> with (chat.chatInfo) { +fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState? = 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 @@ -814,26 +893,57 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo 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, @@ -877,7 +987,8 @@ fun PreviewChatListNavLinkDirect() { disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, - progressByTimeout = false + progressByTimeout = false, + {} ) }, click = {}, @@ -885,7 +996,7 @@ fun PreviewChatListNavLinkDirect() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, ) } } @@ -922,7 +1033,8 @@ fun PreviewChatListNavLinkGroup() { disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, - progressByTimeout = false + progressByTimeout = false, + {} ) }, click = {}, @@ -930,7 +1042,7 @@ fun PreviewChatListNavLinkGroup() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, ) } } @@ -952,7 +1064,7 @@ fun PreviewChatListNavLinkContactRequest() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = 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 d03b8a708d..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,159 +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.TextRange +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 kotlinx.coroutines.flow.distinctUntilChanged -import java.net.URI +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) { + 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 - val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } - val scope = rememberCoroutineScope() - val (userPickerState, scaffoldState ) = settingsState - Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(searchText, scaffoldState.drawerState, userPickerState, stopped)} }, - scaffoldState = scaffoldState, - drawerContent = { - tryOrShowError("Settings", error = { ErrorSettingsView() }) { - SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) - } - }, - contentColor = LocalContentColor.current, - drawerContentColor = LocalContentColor.current, - drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), - drawerGesturesEnabled = appPlatform.isAndroid, - floatingActionButton = { - if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { - 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)) { - Box( - modifier = Modifier - .fillMaxSize() - ) { - if (!chatModel.desktopNoUserNoRemote) { - ChatList(chatModel, searchText = searchText) + NewChatSheet(rh = chatModel.currentRemoteHost.value, close) + DisposableEffect(Unit) { + onDispose { + chatModel.newChatSheetVisible.value = false } - if (chatModel.chats.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) - if (!stopped && !newChatSheetState.collectAsState().value.isVisible() && chatModel.chatRunning.value == true && searchText.value.text.isEmpty()) { - OnboardingButtons(showNewChatSheet) - } - } - } - } - } - if (searchText.value.text.isEmpty()) { - if (appPlatform.isDesktop) { - val call = remember { chatModel.activeCall }.value - if (call != null) { - ActiveCallInteractiveArea(call, 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, 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, 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, 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)) } } @@ -181,9 +342,69 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -private fun ChatListToolbar(searchInList: State, drawerState: DrawerState, userPickerState: MutableStateFlow, stopped: Boolean) { +private fun ChatListToolbar(userPickerState: MutableStateFlow, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { + val serversSummary: MutableState = remember { mutableStateOf(null) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() - if (stopped) { + 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 (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( @@ -199,48 +420,96 @@ private fun ChatListToolbar(searchInList: State, drawerState: Dr } } } + val clipboard = LocalClipboardManager.current val scope = rememberCoroutineScope() - DefaultTopAppBar( + val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } } + DefaultAppBar( navigationButton = { if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { - NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } + 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) { - val enabled = remember { derivedStateOf { searchInList.value.text.isEmpty() } } - if (enabled.value) { - ToggleFilterEnabledButton() - } else { - ToggleFilterDisabledButton() + 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 = false, + onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null, + onTop = !oneHandUI.value, onSearchValueChanged = {}, - buttons = barButtons + 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 @@ -250,7 +519,7 @@ 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) { @@ -270,6 +539,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U } } + @Composable private fun BoxScope.unreadBadge(text: String? = "") { Text( @@ -287,42 +557,34 @@ private fun BoxScope.unreadBadge(text: String? = "") { @Composable private fun ToggleFilterEnabledButton() { - val pref = remember { ChatController.appPrefs.showUnreadAndFavorites } - IconButton(onClick = { pref.set(!pref.get()) }) { + 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 Color.Unspecified, 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 -private fun ToggleFilterDisabledButton() { - IconButton({}, enabled = false) { - Icon( - painterResource(MR.images.ic_filter_list), - null, - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .padding(3.dp) - .border(width = 1.dp, color = MaterialTheme.colors.secondary, shape = RoundedCornerShape(50)) - .padding(3.dp) - .size(16.dp) - ) - } -} +expect fun ActiveCallInteractiveArea(call: Call) -@Composable -expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow) - -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 @@ -335,91 +597,78 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { @Composable private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { - Row(verticalAlignment = Alignment.CenterVertically) { - val focusRequester = remember { FocusRequester() } - var focused by remember { mutableStateOf(false) } - Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), 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() + 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) } - } else { - Row { + 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 - val clipboard = LocalClipboardManager.current - val clipboardHasText = remember(focused) { chatModel.clipboardHasText }.value - if (clipboardHasText) { - IconButton( - onClick = { searchText.value = searchText.value.copy(clipboard.getText()?.text ?: return@IconButton) }, - Modifier.size(30.dp).desktopPointerHoverIconHand() - ) { - Icon(painterResource(MR.images.ic_article), null, tint = MaterialTheme.colors.secondary) - } - } - Spacer(Modifier.width(padding)) - IconButton( - onClick = { - val fixedRhId = chatModel.currentRemoteHost.value - ModalManager.center.closeModals() - ModalManager.center.showModalCloseable { close -> - NewChatView(fixedRhId, selection = NewChatOption.CONNECT, showQRCodeScanner = true, close = close) - } - }, - Modifier.size(30.dp).desktopPointerHoverIconHand() - ) { - Icon(painterResource(MR.images.ic_qr_code), null, tint = MaterialTheme.colors.secondary) + 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 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 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)) } } @@ -427,7 +676,7 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState< withBGApi { planAndConnect( chatModel.remoteHostId(), - URI.create(link), + link, incognito = null, filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id }, @@ -446,98 +695,558 @@ private fun ErrorSettingsView() { private var lazyListState = 0 to 0 +enum class ScrollDirection { + Up, Down, Idle +} + @Composable -private fun ChatList(chatModel: ChatModel, searchText: MutableState) { - 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, 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 searchShowingSimplexLink = remember { mutableStateOf(false) } val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } - val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.toList()) + 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( - Modifier.fillMaxWidth(), - listState + if (!oneHandUI.value) Modifier.imePadding() else Modifier, + listState, + reverseLayout = oneHandUI.value ) { + item { Spacer(Modifier.height(blankSpaceSize)) } stickyHeader { Column( Modifier + .zIndex(1f) .offset { - val y = if (searchText.value.text.isEmpty()) { - if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000 + 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 { - 0 + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset + else -> offsetMultiplier * 1000 + } } IntOffset(0, y) } .background(MaterialTheme.colors.background) - ) { - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - Divider() + ) { + 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) { index, chat -> + 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.isNotEmpty()) { - 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) { + 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, - searchShowingSimplexLink: State, - searchChatFilteredBySimplexLink: State, - searchText: String, - chats: List -): List { - 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() && !showUnreadAndFavorites) - chats - else { - chats.filter { chat -> - when (val cInfo = chat.chatInfo) { - is ChatInfo.Direct -> if (s.isEmpty()) { - chat.id == chatModel.chatId.value || filtered(chat) +private val TAG_MIN_HEIGHT = 35.dp + +@Composable +private fun TagsView(searchText: MutableState) { + 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) + } + } else { + CollapsedTagsFilterView(searchText) + alwaysShownPresetTags.forEach { tag -> + ExpandedTagFilterView(tag.key) + } + } + } + + 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 { - (viewNameContains(cInfo, s) || - cInfo.contact.profile.displayName.lowercase().contains(s) || - cInfo.contact.fullName.lowercase().contains(s)) + 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 + ) } - is ChatInfo.Group -> if (s.isEmpty()) { - chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited - } else { - viewNameContains(cInfo, s) + 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 + ) } - is ChatInfo.Local -> s.isEmpty() || viewNameContains(cInfo, s) - is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s) - is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value) - is ChatInfo.InvalidJSON -> chat.id == chatModel.chatId.value + } + 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 + ) + } + } + } +} + +@Composable +expect fun TagsRow(content: @Composable() (() -> Unit)) + +@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) { + 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) } } } } } -private fun filtered(chat: Chat): Boolean = - (chat.chatInfo.chatSettings?.favorite ?: false) || - chat.chatStats.unreadChat || - (chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0) +@Composable +fun ItemPresetFilterAction( + presetTag: PresetTagKind, + active: Boolean, + showMenu: MutableState, + 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 + } + ) +} -private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean = - cInfo.chatViewName.lowercase().contains(s.lowercase()) +fun filteredChats( + searchShowingSimplexLink: State, + searchChatFilteredBySimplexLink: State, + searchText: String, + chats: List, + activeFilter: ActiveFilter? = null, +): List { + 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 = + 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 336d104d2d..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,18 +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.chatModel -import chat.simplex.common.views.chat.item.markedDeletedText +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 @@ -41,7 +46,8 @@ fun ChatPreviewView( disabled: Boolean, linkMode: SimplexLinkMode, inProgress: Boolean, - progressByTimeout: Boolean + progressByTimeout: Boolean, + defaultClickAction: () -> Unit ) { val cInfo = chat.chatInfo @@ -50,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 ) } @@ -64,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() @@ -87,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 Unit, Map> { + fun messageDraft(draft: ComposeState, sp20: Dp): Pair Unit, Map> { fun attachment(): Pair? = when (draft.preview) { is ComposePreview.FilePreview -> MR.images.ic_draft_filled to draft.preview.fileName @@ -115,12 +122,12 @@ fun ChatPreviewView( "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 inlineContentBuilder to inlineContent @@ -150,7 +157,7 @@ fun ChatPreviewView( else if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary ) - GroupMemberStatus.MemAccepted -> chatPreviewTitleText(MaterialTheme.colors.secondary) + GroupMemberStatus.MemAccepted, GroupMemberStatus.MemRejected -> chatPreviewTitleText(MaterialTheme.colors.secondary) else -> chatPreviewTitleText( if (deleting) MaterialTheme.colors.secondary @@ -167,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) { chatModelDraft.message to 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 -> markedDeletedText(ci.meta) 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, @@ -185,22 +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) { @@ -209,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 -> {} @@ -218,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 ) @@ -244,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 -> @@ -256,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) } @@ -264,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.chatTs) - 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 (disabled || 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) { @@ -360,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) @@ -378,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, @@ -386,6 +603,6 @@ fun unreadCountStr(n: Int): String { @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), disabled = 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, statsStartedAt: Instant, header: String? = null, footer: String? = null, rh: RemoteHostInfo?) { + val sortedServers = servers.sortedWith(compareBy { !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, 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) { + 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 1de1e40afb..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,51 +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.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) }, - click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, + 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, @@ -64,13 +98,15 @@ private fun SharePreviewView(chat: Chat) { horizontalArrangement = Arrangement.spacedBy(4.dp) ) { if (chat.chatInfo is ChatInfo.Local) { - ProfileImage(size = 46.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor) + 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 = 46.dp, chat.chatInfo.image) + 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 a36930f5ce..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 @@ -12,46 +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.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.views.helpers.* 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), - contentColor = LocalContentColor.current, - drawerContentColor = LocalContentColor.current, - 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 @@ -62,13 +88,12 @@ private fun EmptyList() { } @Composable -private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableStateFlow, 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 { @@ -78,7 +103,24 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState .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(onButtonClicked = { @@ -91,40 +133,17 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState }) } } - if (chatModel.chats.size >= 8) { - barButtons.add { - IconButton({ showSearch = true }) { - Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), 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, - ) - } - } - } - 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) - is SharedContent.Forward -> stringResource(MR.strings.forward_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, @@ -134,29 +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) { +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 { - val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local } - if (search.isEmpty()) { - sorted.filter { it.chatInfo.ready } - } else { - sorted.filter { it.chatInfo.ready && it.chatInfo.chatViewName.lowercase().contains(search.lowercase()) } - } + val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local } + filteredChats(mutableStateOf(false), mutableStateOf(null), search, sorted) } } + val topPaddingToContent = topPaddingToContent(false) LazyColumnWithScrollBar( - modifier = Modifier.fillMaxWidth() + 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) { + 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(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, saving: MutableState) { + 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, saving: MutableState) { + 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) { + 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) { + 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, showError: State, emoji: MutableState) + +@Composable +fun TagListNameTextField(name: MutableState, showError: State) { + 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 = 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) { + 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) { + 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 23272fcc0c..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,40 +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.* import kotlinx.coroutines.flow.* -import kotlin.math.roundToInt + +private val USER_PICKER_SECTION_SPACING = 32.dp @Composable fun UserPicker( chatModel: ChatModel, userPickerState: MutableStateFlow, - 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 { @@ -56,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 { @@ -65,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() @@ -122,109 +137,122 @@ fun UserPicker( } } } - 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) { - withBGApi { - 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)) - } - if (chatModel.desktopNoUserNoRemote) { - CreateInitialProfile { + } + + 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) { @@ -234,16 +262,135 @@ fun UserPicker( } } } - Divider(Modifier.requiredHeight(1.dp)) - } - } - if (showSettings) { - SettingsPickerItem(settingsClicked) - } - if (showCancel) { - CancelPickerItem(cancelClicked) + ) + } 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, + ) + } + } + } + } + } + } +} + +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 +private fun GlobalSettingsSection( + userPickerState: MutableStateFlow, + 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() } } @@ -251,7 +398,7 @@ fun UserPicker( fun UserProfilePickerItem( u: User, unreadCount: Int = 0, - enabled: Boolean = chatModel.chatRunning.value == true || chatModel.connectedToRemote, + enabled: Boolean = remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit @@ -259,7 +406,7 @@ fun UserProfilePickerItem( Row( Modifier .fillMaxWidth() - .sizeIn(minHeight = 46.dp) + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) .combinedClickable( enabled = enabled, onClick = if (u.activeUser) openSettings else onClick, @@ -275,32 +422,41 @@ fun UserProfilePickerItem( 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, enabled: Boolean = chatModel.chatRunning.value == true || chatModel.connectedToRemote) { +fun UserProfileRow(u: User, enabled: Boolean = remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote) { Row( Modifier .widthIn(max = windowWidth() * 0.7f) @@ -309,7 +465,7 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr ) { ProfileImage( image = u.image, - size = 54.dp + size = 54.dp * fontSizeSqrtMultiplier ) Text( u.displayName, @@ -322,136 +478,144 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr } @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, + onLocalDeviceClick: () -> Unit, + onRemoteHostClick: (rh: RemoteHostInfo, connecting: MutableState) -> 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 MenuTextColor, - fontSize = 14.sp, - ) - } -} +expect fun UserPickerUsersSection( + users: List, + iconColor: Color, + stopped: Boolean, + onUserClicked: (user: User) -> Unit, +) @Composable -fun LocalDevicePickerItem(active: Boolean, onLongClick: () -> Unit = {}, onClick: () -> Unit) { +expect fun PlatformUserPicker( + modifier: Modifier, + pickerState: MutableStateFlow, + 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 MenuTextColor, - 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 = MenuTextColor) - } -} - -@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 = MenuTextColor) - } -} - -@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 = MenuTextColor) - } -} - -@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 = MenuTextColor) - } -} - -@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 = MenuTextColor) + 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 + ) + } + } + } } } @@ -459,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, @@ -469,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) { + 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, 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) { + 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 6846d1c735..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 = { withLongRunningApi { saveArchiveLauncher.launch(archivePath.substringAfterLast(File.separator)) }}, - deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) } - ) -} - -@Composable -fun ChatArchiveLayout( - title: String, - archiveTime: Instant, - saveArchive: () -> Unit, - deleteArchiveAlert: () -> Unit -) { - ColumnWithScrollBar( - 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 ca14b19adf..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 @@ -27,6 +27,7 @@ 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.* @@ -40,21 +41,20 @@ import kotlin.math.log2 @Composable 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, @@ -64,8 +64,19 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { progressIndicator, migration, onConfirmEncrypt = { - withLongRunningApi { - encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator, migration) + // 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 } } ) @@ -89,7 +100,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { @Composable fun DatabaseEncryptionLayout( useKeychain: MutableState, - prefs: AppPreferences, chatDbEncrypted: Boolean?, currentKey: MutableState, newKey: MutableState, @@ -100,101 +110,104 @@ fun DatabaseEncryptionLayout( migration: Boolean, onConfirmEncrypt: () -> Unit, ) { - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - if (!migration) Modifier.fillMaxWidth().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier) else Modifier.fillMaxWidth(), - ) { - 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, prefs, migration) - } else if (storedKey.value && !migration) { - // Don't show in migration process since it will remove the key after successful encryption - removePassphraseAlert { - removePassphraseFromKeyChain(useKeychain, prefs, storedKey, false) - } - } else { - setUseKeychain(false, useKeychain, prefs, migration) - } + @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(if (migration) MR.strings.set_passphrase else 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, migration) - } - SectionBottomSpacer() } - if (appPlatform.isDesktop && !migration) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) + if (migration) { + Column(Modifier.fillMaxWidth()) { + Layout() + } + } else { + ColumnWithScrollBar(maxIntrinsicSize = true) { + Layout() } } } @@ -272,17 +285,17 @@ fun resetFormAfterEncryption( m.controller.appPrefs.initialRandomDBPassphrase.set(false) } -fun setUseKeychain(value: Boolean, useKeychain: MutableState, prefs: AppPreferences, migration: Boolean) { +fun setUseKeychain(value: Boolean, useKeychain: MutableState, migration: Boolean) { useKeychain.value = value // Postpone it when migrating to the end of encryption process if (!migration) { - prefs.storeDBPassphrase.set(value) + appPrefs.storeDBPassphrase.set(value) } } -private fun removePassphraseFromKeyChain(useKeychain: MutableState, prefs: AppPreferences, storedKey: MutableState, migration: Boolean) { +private fun removePassphraseFromKeyChain(useKeychain: MutableState, storedKey: MutableState, migration: Boolean) { DatabaseUtils.ksDatabasePassword.remove() - setUseKeychain(false, useKeychain, prefs, migration) + setUseKeychain(false, useKeychain, migration) storedKey.value = false } @@ -296,7 +309,6 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState, @@ -415,16 +427,15 @@ suspend fun encryptDatabase( 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) { @@ -438,7 +449,7 @@ 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 @@ -451,8 +462,8 @@ suspend fun encryptDatabase( resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) if (useKeychain.value) { DatabaseUtils.ksDatabasePassword.set(new) - } else if (migration) { - removePassphraseFromKeyChain(useKeychain, prefs, storedKey, true) + } else { + removePassphraseFromKeyChain(useKeychain, storedKey, migration) } operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted)) @@ -523,7 +534,6 @@ fun PreviewDatabaseEncryptionLayout() { SimpleXTheme { DatabaseEncryptionLayout( useKeychain = remember { mutableStateOf(true) }, - prefs = AppPreferences(), chatDbEncrypted = true, currentKey = remember { mutableStateOf("") }, newKey = remember { mutableStateOf("") }, 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 5f0356bb2d..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 @@ -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(", "))) } - ColumnWithScrollBar( - Modifier.fillMaxSize(), - 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 -> {} } @@ -294,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 40dfbeac73..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 @@ -12,54 +12,57 @@ 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(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) + } } } } @@ -70,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, progressIndicator) }, stopChatAlert = { stopChatAlert(m, progressIndicator) }, - exportArchive = { exportArchive(m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) }, - deleteChatAlert = { deleteChatAlert(m, progressIndicator) }, - deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(appFilesCountAndSize) }, + 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) { @@ -100,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 -> @@ -127,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, importArchiveLauncher: FileChooserLauncher, - chatArchiveName: MutableState, - chatArchiveTime: MutableState, - chatLastStart: MutableState, appFilesCountAndSize: MutableState>, chatItemTTL: MutableState, currentUser: User?, @@ -148,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 - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_chat_database)) if (!chatModel.desktopNoUserNoRemote) { @@ -179,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 && !progressIndicator, 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)) { @@ -208,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, false) }, + click = { ModalManager.start.showModal { DatabaseEncryptionView(chatModel, false) } }, iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary, disabled = operationsDisabled ) @@ -226,6 +232,9 @@ fun DatabaseLayout( click = { if (initialRandomDBPassphrase.get()) { exportProhibitedAlert() + ModalManager.start.showModal { + DatabaseEncryptionView(chatModel, false) + } } else { exportArchive() } @@ -242,18 +251,6 @@ fun DatabaseLayout( 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), @@ -263,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 @@ -306,21 +303,25 @@ private fun setChatItemTTLAlert( } @Composable -private fun TtlOptions(current: State, enabled: State, onSelected: (ChatItemTTL) -> Unit) { +fun TtlOptions( + current: State, + enabled: State, + onSelected: (ChatItemTTL?) -> Unit, + default: State? = null +) { val values = remember { - val all: ArrayList = arrayListOf(ChatItemTTL.None, ChatItemTTL.Month, ChatItemTTL.Week, ChatItemTTL.Day) - if (current.value is ChatItemTTL.Seconds) { - all.add(current.value) + val all: ArrayList = 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> = 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), @@ -334,7 +335,6 @@ private fun TtlOptions(current: State, enabled: State, onS @Composable fun RunChatSetting( - runChat: Boolean, stopped: Boolean, enabled: Boolean, startChat: () -> Unit, @@ -347,7 +347,7 @@ fun RunChatSetting( iconColor = if (stopped) Color.Red else MaterialTheme.colors.primary, ) { DefaultSwitch( - checked = runChat, + checked = !stopped, onCheckedChange = { runChatSwitch -> if (runChatSwitch) { startChat() @@ -360,12 +360,12 @@ 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) -} - -fun startChat(m: ChatModel, chatLastStart: MutableState, chatDbChanged: MutableState, progressIndicator: MutableState? = null) { +fun startChat( + m: ChatModel, + chatLastStart: MutableState, + chatDbChanged: MutableState, + progressIndicator: MutableState? = null +) { withLongRunningApi { try { progressIndicator?.value = true @@ -422,6 +422,7 @@ fun authStopChat(m: ChatModel, progressIndicator: MutableState? = null, 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 -> { @@ -448,6 +449,11 @@ private fun stopChat(m: ChatModel, progressIndicator: MutableState? = n 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 @@ -464,6 +470,40 @@ suspend fun stopChatAsync(m: ChatModel) { controller.appPrefs.chatStopped.set(true) } +fun stopChatRunBlockStartChat( + stopped: Boolean, + chatLastStart: MutableState, + progressIndicator: MutableState, + 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() @@ -486,142 +526,182 @@ fun deleteChatDatabaseFilesAndState() { 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 - chatModel.chatItems.clear() - chatModel.chats.clear() - chatModel.users.clear() -} - -private fun exportArchive( - m: ChatModel, - progressIndicator: MutableState, - chatArchiveName: MutableState, - chatArchiveTime: MutableState, - chatArchiveFile: MutableState, - saveArchiveLauncher: FileChooserLauncher -) { - progressIndicator.value = true withLongRunningApi { - try { - val archiveFile = exportChatArchive(m, null, 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 + 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 exportArchive( + m: ChatModel, + progressIndicator: MutableState, + chatArchiveFile: MutableState, + 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?, - chatArchiveName: MutableState, - chatArchiveTime: MutableState, chatArchiveFile: MutableState -): String { +): Pair> { 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 = "${(storagePath ?: filesDir).absolutePath}${File.separator}$archiveName" + val archivePath = "${(storagePath ?: databaseExportDir).absolutePath}${File.separator}$archiveName" val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) // 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()) } - m.controller.apiExportArchive(config) + wallpapersDir.mkdirs() + val archiveErrors = m.controller.apiExportArchive(config) if (storagePath == null) { - deleteOldArchive(m) + deleteOldChatArchive() m.controller.appPrefs.chatArchiveName.set(archiveName) m.controller.appPrefs.chatArchiveTime.set(archiveTime) } - chatArchiveName.value = archiveName - chatArchiveTime.value = 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>, - progressIndicator: MutableState, - 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) { + 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, 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): 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>, progressIndicator: MutableState, - startChat: () -> Unit, -) { + migration: Boolean +): Boolean { + val m = chatModel progressIndicator.value = true val archivePath = saveArchiveFromURI(importedArchiveURI) if (archivePath != null) { - withLongRunningApi { + 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? { @@ -631,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) { +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) { +private suspend fun deleteChat(m: ChatModel, progressIndicator: MutableState) { + if (!DatabaseUtils.hasAtLeastOneDatabase(dataDir.absolutePath)) { + return + } progressIndicator.value = true - withBGApi { - 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()) } } } @@ -702,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}") @@ -713,12 +795,12 @@ private fun afterSetCiTTL( } } -private fun deleteFilesAndMediaAlert(appFilesCountAndSize: MutableState>) { +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 ) } @@ -743,18 +825,13 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState Unit)? = null, hostDevice: Pair? = null, + belowTextContent: @Composable (() -> Unit) = {}, buttons: @Composable () -> Unit, ) { showAlert { AlertDialog( - onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, + onDismissRequest = { onDismissRequest?.invoke(); if (dismissible) hideAlert() }, title = alertTitle(title), buttons = { - AlertContent(text, hostDevice, extraPadding = true) { + AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) { buttons() } }, @@ -286,7 +289,14 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? { } @Composable -private fun AlertContent(text: String?, hostDevice: Pair?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { +private fun AlertContent( + text: String?, + hostDevice: Pair?, + extraPadding: Boolean = false, + textAlign: TextAlign = TextAlign.Center, + belowTextContent: @Composable (() -> Unit) = {}, + content: @Composable (() -> Unit) +) { BoxWithConstraints { Column( Modifier @@ -300,17 +310,20 @@ private fun AlertContent(text: String?, hostDevice: Pair?, 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 chatListAnimationSpec() = tween(durationMillis = 250, easing = FastOu fun newChatSheetAnimSpec() = tween(256, 0, LinearEasing) fun audioProgressBarAnimationSpec() = tween(durationMillis = 30, easing = LinearEasing) + +fun userPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) + +fun mentionPickerAnimSpec() = tween(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? = 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, 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, + prefAlpha: State, + 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, + 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, + keyboardCoversBar: Boolean, + onTop: Boolean, + graphicsLayer: GraphicsLayer, + backgroundGraphicsLayer: GraphicsLayer, + backgroundGraphicsLayerSize: State, + 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 34d916781b..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,19 +2,22 @@ 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.* 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 chat.simplex.common.model.ChatInfo @@ -25,10 +28,15 @@ 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 = when (chatInfo) { - is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled + 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 } @@ -51,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) { @@ -61,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), @@ -81,7 +94,7 @@ fun ProfileImage( imageBitmap, stringResource(MR.strings.image_descr_profile_image), contentScale = ContentScale.Crop, - modifier = ProfileIconModifier(size) + modifier = ProfileIconModifier(size, blurred = blurred) ) } } @@ -102,12 +115,12 @@ private const val squareToCircleRatio = 0.935f private const val radiusFactor = (1 - squareToCircleRatio) / 50 @Composable -fun ProfileIconModifier(size: Dp, padding: Boolean = true): Modifier { +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) - return when { + val m1 = when { r >= 50 -> m.padding(pad).clip(CircleShape) r <= 0 -> { @@ -119,6 +132,7 @@ fun ProfileIconModifier(size: Dp, padding: Boolean = true): Modifier { 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. @@ -129,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), 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, + val tint: Map, + val colors: Map, +) { + 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 = + 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 = 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? = 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, 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 2fb27e29b1..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, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, 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(tintColor = tintColor, onButtonClicked = close) - } else { - Spacer(Modifier) - } - Row { - endButtons() - } - } - } - ) - } -} - -@Composable -fun AppBarTitle(title: String, hostDevice: Pair? = 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, 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 = 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?, 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 = 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/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index 0ad7af439f..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) 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, + 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/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 577411c7e3..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 = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f), - 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)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) { +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 = tintColor + 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(null) + private set + + internal val scrollChannel = Channel() + + 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(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 ee4638445b..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,8 +2,7 @@ package chat.simplex.common.views.helpers import androidx.compose.runtime.saveable.Saver -import chat.simplex.common.model.ChatInfo -import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.* import kotlinx.serialization.descriptors.* @@ -15,7 +14,7 @@ sealed class SharedContent { data class Text(val text: String): SharedContent() data class Media(val text: String, val uris: List): SharedContent() data class File(val text: String, val uri: URI): SharedContent() - data class Forward(val chatItem: ChatItem, val fromChatInfo: ChatInfo): SharedContent() + data class Forward(val chatItems: List, 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 7e57bda928..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,6 +13,7 @@ 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 @@ -50,7 +55,7 @@ fun 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 ) @@ -85,10 +90,12 @@ fun ExposedDropDownSettingWithIcon( values: List>, selection: State, fontSize: TextUnit = 16.sp, - iconSize: Dp = 40.dp, + iconPaddingPercent: Float = 0.2f, listIconSize: Dp = 30.dp, + boxSize: Dp = 60.dp, iconColor: Color = MenuTextColor, enabled: State = mutableStateOf(true), + background: Color, minWidth: Dp = 200.dp, onSelected: (T) -> Unit ) { @@ -99,13 +106,22 @@ fun ExposedDropDownSettingWithIcon( expanded.value = !expanded.value && enabled.value } ) { - Row( - Modifier.padding(start = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End + 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.size(iconSize), tint = iconColor) + Icon(painterResource(choice.second), choice.third, Modifier.padding(boxSize * iconPaddingPercent).fillMaxSize(), tint = iconColor) } DefaultExposedDropdownMenu( modifier = Modifier.widthIn(min = minWidth), 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, 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 022ee37589..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 @@ -34,6 +34,7 @@ expect fun authenticate( promptSubtitle: String, selfDestruct: Boolean = false, usingLAMode: LAMode = ChatModel.controller.appPrefs.laMode.get(), + oneTime: Boolean, completed: (LAResult) -> Unit ) @@ -41,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, contentColor = LocalContentColor.current) { + 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 887a5bfdd9..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,32 +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, + showAppBar: Boolean = true, enableClose: Boolean = true, - background: Color = MaterialTheme.colors.background, + 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) { + if (showClose && showAppBar) { BackHandler(enabled = enableClose, onBack = close) } + val oneHandUI = remember { derivedStateOf { if (appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete) appPrefs.oneHandUI.state.value else false } } Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { - Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { - CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons) - Box(modifier) { content() } + 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 + ) + } + } + } } } } @@ -40,38 +73,58 @@ enum class ModalPlacement { START, CENTER, END, FULLSCREEN } -class ModalData { - private val state = mutableMapOf>() +class ModalData(val keyboardCoversBar: Boolean = true) { + private val state = mutableMapOf>() fun stateGetOrPut (key: String, default: () -> T): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState + + fun stateGetOrPutNullable (key: String, default: () -> T?): MutableState = + state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState + + val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar) +} + +enum class ModalViewId { + SECONDARY_CHAT } class ModalManager(private val placement: ModalPlacement? = null) { - private val modalViews = arrayListOf 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() + private val _modalCount = mutableStateOf(0) + val modalCount: State = _modalCount private val toRemove = mutableSetOf() private var oldViewChanging = AtomicBoolean(false) // 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 ModalData.() -> Unit) { - val data = ModalData() - showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.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, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { - val data = ModalData() - showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.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 ModalData.(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() + 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()) { @@ -80,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(Triple(anim, data, 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() { @@ -121,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()?.let { it.third(it.second, ::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, @@ -134,7 +207,11 @@ class ModalManager(private val placement: ModalPlacement? = null) { }.using(SizeTransform(clip = false)) } ) { - modalViews.getOrNull(it - 1)?.let { it.third(it.second, ::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 } } @@ -147,6 +224,11 @@ class ModalManager(private val placement: ModalPlacement? = null) { passcodeView.collectAsState().value?.invoke { passcodeView.value = null } } + @Composable + fun showOneTimePasscodeInView() { + onTimePasscodeView.collectAsState().value?.invoke { onTimePasscodeView.value = null } + } + /** * Allows to modify a list without getting [ConcurrentModificationException] * */ @@ -157,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 animationSpec() = tween(durationMillis = 250, easing = FastOutSlowInEasing) // private fun animationSpecFromStart() = tween(durationMillis = 150, easing = FastOutLinearInEasing) // private fun animationSpecFromEnd() = tween(durationMillis = 100, easing = FastOutSlowInEasing) @@ -189,11 +249,36 @@ private fun animationSpec() = tween(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 animationSpec() = tween(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/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index ca451bd3a2..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,16 +18,15 @@ 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.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +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, @@ -37,6 +35,7 @@ fun SearchTextField( 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() } @@ -50,10 +49,23 @@ 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() + } } } @@ -63,18 +75,24 @@ fun SearchTextField( 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 @@ -85,18 +103,14 @@ fun SearchTextField( 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, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis) }, trailingIcon = if (searchText.value.text.isNotEmpty()) {{ IconButton({ @@ -106,7 +120,7 @@ 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 trailingContent, 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 c96a277fb8..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 SectionViewSelectable( title: String?, @@ -76,16 +94,36 @@ fun SectionViewSelectable( SectionTextFooter(values.first { it.value == currentValue.value }.description) } +@Composable +fun SectionViewSelectableCards( + title: String?, + currentValue: State, + values: List>, + 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 @@ -100,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) @@ -111,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() @@ -127,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) @@ -159,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, @@ -232,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) ) } @@ -254,8 +288,8 @@ fun TextIconSpaced(extraPadding: Boolean = false) { } @Composable -fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground) { - 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) @@ -265,6 +299,61 @@ fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color } } +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, + 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, + 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(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 9a6e3a0f9a..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 @@ -8,17 +8,21 @@ 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.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 @@ -145,6 +149,7 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri runCatching { return yaml.decodeFromStream(it!!) }.onFailure { + Log.e(TAG, "Error while decoding theme: ${it.stackTraceToString()}") if (withAlertOnException) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.import_theme_error), @@ -242,13 +247,26 @@ fun saveAnimImage(uri: URI): CryptoFile? { expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? -fun saveFileFromUri(uri: URI, 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 -> @@ -280,8 +298,65 @@ fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile? } } -fun 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 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 { @@ -291,11 +366,12 @@ fun 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 { @@ -444,12 +520,11 @@ inline fun serializableSaver(): Saver = 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) @@ -485,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 Flow.throttleLatest(delayMillis: Long): Flow = this + .conflate() + .transform { + emit(it) + delay(delayMillis) + } + @Composable fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { DisposableEffect(Unit) { @@ -530,9 +627,11 @@ fun 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) } } } @@ -550,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 5a37c860a0..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 @@ -34,7 +34,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { } else { val r: LAResult = if (passcode.value == authRequest.password) { if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } LAResult.Success } else { 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 index f8e3d48625..03542ca8af 100644 --- 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 @@ -4,7 +4,6 @@ import SectionBottomSpacer import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -17,6 +16,7 @@ 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 @@ -45,16 +45,20 @@ data class MigrationFileLinkData( ) { @Serializable data class NetworkConfig( - val socksProxy: String?, + // Legacy. Remove in 2025 + @SerialName("socksProxy") + val legacySocksProxy: String?, + val networkProxy: NetworkProxy?, val hostMode: HostMode?, val requiredHostMode: Boolean? ) { - fun hasOnionConfigured(): Boolean = socksProxy != null || hostMode == HostMode.Onion + fun hasProxyConfigured(): Boolean = networkProxy != null || legacySocksProxy != null || hostMode == HostMode.Onion fun transformToPlatformSupported(): NetworkConfig { return if (hostMode != null && requiredHostMode != null) { NetworkConfig( - socksProxy = if (hostMode == HostMode.Onion) socksProxy ?: NetCfg.proxyDefaults.socksProxy else socksProxy, + 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 ) @@ -145,20 +149,11 @@ private fun MigrateFromDeviceLayout( ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).height(IntrinsicSize.Max), - ) { + ColumnWithScrollBar(maxIntrinsicSize = true) { AppBarTitle(stringResource(MR.strings.migrate_from_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver) SectionBottomSpacer() } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) - } - } platform.androidLockPortraitOrientation() } @@ -179,7 +174,7 @@ private fun SectionByState( 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) + is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl, chatReceiver.value) is MigrationFromState.Finished -> migrationState.FinishedView(s.chatDeletion) } } @@ -340,7 +335,7 @@ private fun LinkCreationView() { } @Composable -private fun MutableState.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) { +private fun MutableState.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl, chatReceiver: MigrationFromChatReceiver?) { SectionView { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_close), @@ -355,7 +350,15 @@ private fun MutableState.LinkShownView(fileId: Long, link: S text = stringResource(MR.strings.migrate_from_device_finalize_migration), textColor = MaterialTheme.colors.primary, click = { - finishMigration(fileId, ctrl) + 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)) @@ -426,7 +429,8 @@ 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) else Modifier.size(windowWidth() - DEFAULT_PADDING * 2)) + (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 @@ -446,6 +450,7 @@ private fun MutableState.stopChat() { 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), @@ -463,12 +468,12 @@ private suspend fun MutableState.verifyDatabasePassphrase(db val error = controller.testStorageEncryption(dbKey) if (error == null) { state = MigrationFromState.UploadConfirmation - } else if (((error.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) { + } 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.details + text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.string ) } } @@ -477,12 +482,13 @@ private fun MutableState.exportArchive() { withLongRunningApi { try { getMigrationTempFilesDirectory().mkdir() - val archivePath = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf("")) - val totalBytes = File(archivePath).length() - if (totalBytes > 0L) { - state = MigrationFromState.DatabaseInit(totalBytes, archivePath) + val (archivePath, archiveErrors) = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf("")) + if (archiveErrors.isEmpty()) { + uploadArchive(archivePath) } else { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist)) + showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_migrate), archiveErrors) { + uploadArchive(archivePath) + } state = MigrationFromState.UploadConfirmation } } catch (e: Exception) { @@ -495,14 +501,28 @@ private fun MutableState.exportArchive() { } } +private fun MutableState.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? { val (status, ctrl) = chatInitTemporaryDatabase(tempDatabaseFile.absolutePath) showErrorOnMigrationIfNeeded(status) try { if (ctrl != null) { val user = startChatWithTemporaryDatabase(ctrl, netCfg) - return if (user != null) ctrl to user else null + 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()}") } @@ -536,11 +556,12 @@ private fun MutableState.startUploading( ) { withBGApi { chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg -> - when (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(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user) + state = MigrationFromState.UploadProgress(r.sentSize, r.totalSize, r.fileTransferMeta.fileId, archivePath, ctrl, user) } } is CR.SndFileRedirectStartXFTP -> { @@ -552,12 +573,13 @@ private fun MutableState.startUploading( val cfg = getNetCfg() val data = MigrationFileLinkData( networkConfig = MigrationFileLinkData.NetworkConfig( - socksProxy = cfg.socksProxy, + legacySocksProxy = null, + networkProxy = if (appPrefs.networkUseSocksProxy.get()) appPrefs.networkProxy.get() else null, hostMode = cfg.hostMode, requiredHostMode = cfg.requiredHostMode ) ) - state = MigrationFromState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl) + state = MigrationFromState.LinkShown(r.fileTransferMeta.fileId, data.addToLink(r.rcvURIs[0]), ctrl) } is CR.SndFileError -> { AlertManager.shared.showAlertMsg( @@ -597,9 +619,11 @@ private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) { } } -private fun MutableState.finishMigration(fileId: Long, ctrl: ChatCtrl) { +private fun MutableState.finishMigration(fileId: Long, ctrl: ChatCtrl, chatReceiver: MigrationFromChatReceiver?) { withBGApi { cancelUploadedArchive(fileId, ctrl) + chatReceiver?.stopAndCleanUp() + getMigrationTempFilesDirectory().deleteRecursively() state = MigrationFromState.Finished(false) } } @@ -635,6 +659,7 @@ private suspend fun startChatAndDismiss(dismiss: Boolean = true) { } else if (user != null) { startChat(user) } + platform.androidChatStartedAfterBeingOff() } catch (e: Exception) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.error_starting_chat), @@ -668,7 +693,7 @@ private class MigrationFromChatReceiver( val ctrl: ChatCtrl, val databaseUrl: File, var receiveMessages: Boolean = true, - val processReceivedMsg: suspend (CR) -> Unit + val processReceivedMsg: suspend (API) -> Unit ) { fun start() { Log.d(TAG, "MigrationChatReceiver startReceiver") @@ -677,19 +702,18 @@ private class MigrationFromChatReceiver( try { val msg = ChatController.recvMsg(ctrl) if (msg != null && receiveMessages) { - val r = msg.resp - val rhId = msg.remoteHostId - Log.d(TAG, "processReceivedMsg: ${r.responseType}") - chatModel.addTerminalItem(TerminalItem.resp(rhId, r)) + val rhId = msg.rhId + Log.d(TAG, "processReceivedMsg: ${msg.responseType}") + chatModel.addTerminalItem(TerminalItem.resp(rhId, msg)) val finishedWithoutTimeout = withTimeoutOrNull(60_000L) { - processReceivedMsg(r) + processReceivedMsg(msg) } if (finishedWithoutTimeout == null) { - Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType) + 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.resp.responseType + "\n" + Exception().stackTraceToString()), + text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()), shareText = true ) } 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 index 9d204ad42c..d74846f8a3 100644 --- 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 @@ -5,16 +5,15 @@ import SectionItemView import SectionSpacer 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.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 @@ -27,6 +26,7 @@ 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 @@ -35,16 +35,17 @@ 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 hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState() - @Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState() - @Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState() - @Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): 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 @@ -66,10 +67,10 @@ sealed class MigrationToDeviceState { null } else { val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName) - MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg) + MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg, state.networkProxy) } } - is Passphrase -> MigrationToState.Passphrase("", state.netCfg) + is Passphrase -> MigrationToState.Passphrase("", state.netCfg, state.networkProxy) } if (initial == null) { settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE) @@ -91,16 +92,24 @@ sealed class MigrationToDeviceState { @Serializable sealed class MigrationToState { @Serializable object PasteOrScanLink: MigrationToState() - @Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState() - @Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState() - @Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState() - @Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState() - @Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState() - @Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState() - @Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState() - @Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState() - @Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState() - @Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): 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.state: MigrationToState? @@ -155,20 +164,11 @@ private fun ModalData.MigrateToDeviceLayout( close: () -> Unit, ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).height(IntrinsicSize.Max), - ) { + ColumnWithScrollBar(maxIntrinsicSize = true) { AppBarTitle(stringResource(MR.strings.migrate_to_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close) SectionBottomSpacer() } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) - } - } platform.androidLockPortraitOrientation() } @@ -181,35 +181,45 @@ private fun ModalData.SectionByState( ) { when (val s = migrationState.value) { null -> {} - is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView() - is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState) - is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg) - is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg) + 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) - is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg) - is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg) - is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg) - is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg) - is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close) + 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.PasteOrScanLinkView() { - if (appPlatform.isAndroid) { - SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) { - QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text -> - withBGApi { checkUserLink(text) } +private fun MutableState.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) } } - SectionSpacer() - } - - if (appPlatform.isDesktop || appPreferences.developerTools.get()) { - SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) { - PasteLinkView() - } + if (progressIndicator.value) + ProgressView() } } @@ -225,22 +235,49 @@ private fun MutableState.PasteLinkView() { } @Composable -private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState) { +private fun ArchiveImportView(progressIndicator: MutableState, 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) { val onionHosts = remember { stateGetOrPut("onionHosts") { - getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts + getNetCfg().copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts } } - val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { socksProxy != null } } + val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { linkNetworkProxy != null || legacyLinkSocksProxy != null } } val sessionMode = remember { stateGetOrPut("sessionMode") { TransportSessionMode.User} } - val networkProxyHostPort = remember { stateGetOrPut("networkHostProxyPort") { - var proxy = (socksProxy ?: chatModel.controller.appPrefs.networkProxyHostPort.get()) - if (proxy?.startsWith(":") == true) proxy = "localhost$proxy" - proxy + 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 proxyPort = remember { derivedStateOf { networkProxyHostPort.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } val netCfg = rememberSaveable(stateSaver = serializableSaver()) { - mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value)) + mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, sessionMode = sessionMode.value)) } SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) { @@ -251,12 +288,12 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos click = { val updated = netCfg.value .withOnionHosts(onionHosts.value) - .withHostPort(if (networkUseSocksProxy.value) networkProxyHostPort.value else null, null) + .withProxy(if (networkUseSocksProxy.value) networkProxy.value else null, null) .copy( sessionMode = sessionMode.value ) withBGApi { - state.value = MigrationToState.DatabaseInit(link, updated) + state.value = MigrationToState.DatabaseInit(link, updated, if (networkUseSocksProxy.value) networkProxy.value else null) } } ){} @@ -265,8 +302,8 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos SectionSpacer() - val networkProxyHostPortPref = SharedPreference(get = { networkProxyHostPort.value }, set = { - networkProxyHostPort.value = it + val networkProxyPref = SharedPreference(get = { networkProxy.value }, set = { + networkProxy.value = it }) SectionView(stringResource(MR.strings.network_settings_title).uppercase()) { OnionRelatedLayout( @@ -274,14 +311,10 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos networkUseSocksProxy, onionHosts, sessionMode, - networkProxyHostPortPref, - proxyPort, + networkProxyPref, toggleSocksProxy = { enable -> networkUseSocksProxy.value = enable }, - useOnion = { - onionHosts.value = it - }, updateSessionMode = { sessionMode.value = it } @@ -290,13 +323,13 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos } @Composable -private fun MutableState.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) { +private fun MutableState.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) + prepareDatabase(link, tempDatabaseFile, netCfg, networkProxy) } } @@ -308,14 +341,15 @@ private fun MutableState.LinkDownloadingView( archivePath: String, tempDatabaseFile: File, chatReceiver: MutableState, - netCfg: NetCfg + 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) + startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg, networkProxy) } } @@ -330,14 +364,14 @@ private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) { } @Composable -private fun MutableState.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) { +private fun MutableState.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) + state = MigrationToState.DatabaseInit(link, netCfg, networkProxy) } ) {} SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again)) @@ -350,25 +384,25 @@ private fun MutableState.DownloadFailedView(link: String, cha } @Composable -private fun MutableState.ArchiveImportView(archivePath: String, netCfg: NetCfg) { +private fun MutableState.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { Box { SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {} ProgressView() } LaunchedEffect(Unit) { - importArchive(archivePath, netCfg) + importArchive(archivePath, netCfg, networkProxy) } } @Composable -private fun MutableState.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) { +private fun MutableState.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) + state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy) } ) {} SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again)) @@ -376,7 +410,7 @@ private fun MutableState.ArchiveImportFailedView(archivePath: } @Composable -private fun MutableState.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) { +private fun MutableState.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { val currentKey = rememberSaveable { mutableStateOf(currentKey) } val verifyingPassphrase = rememberSaveable { mutableStateOf(false) } val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) } @@ -406,9 +440,9 @@ private fun MutableState.PassphraseEnteringView(currentKey: S 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) + 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) + state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg, networkProxy) } else { showErrorOnMigrationIfNeeded(status) } @@ -425,7 +459,7 @@ private fun MutableState.PassphraseEnteringView(currentKey: S } @Composable -private fun MutableState.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) { +private fun MutableState.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) { data class Tuple4(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) { @@ -460,7 +494,7 @@ private fun MutableState.MigrationConfirmationView(status: DB text = button, textColor = MaterialTheme.colors.primary, click = { - state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg) + state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg, networkProxy) } ) {} } @@ -469,13 +503,13 @@ private fun MutableState.MigrationConfirmationView(status: DB } @Composable -private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) { +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, close) + startChat(passphrase, confirmation, useKeychain, netCfg, networkProxy, close) } } @@ -484,28 +518,32 @@ private fun ProgressView() { DefaultProgressView(null) } -private suspend fun MutableState.checkUserLink(link: String) { - if (strHasSimplexFileLink(link.trim())) { +private suspend fun MutableState.checkUserLink(link: String): Boolean { + return if (strHasSimplexFileLink(link.trim())) { val data = MigrationFileLinkData.readFromLink(link) - val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false + val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false val networkConfig = data?.networkConfig?.transformToPlatformSupported() // If any of iOS or Android had onion enabled, show onion screen - if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) { - state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode) - MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)) + 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 = networkConfig?.socksProxy, + 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 } } @@ -513,16 +551,19 @@ private fun MutableState.prepareDatabase( link: String, tempDatabaseFile: File, netCfg: NetCfg, + networkProxy: NetworkProxy? ) { withLongRunningApi { val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg) if (ctrlAndUser == null) { - state = MigrationToState.DownloadFailed(0, link, archivePath(), netCfg) + // 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) + state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg, networkProxy) } } @@ -535,40 +576,42 @@ private fun MutableState.startDownloading( link: String, archivePath: String, netCfg: NetCfg, + networkProxy: NetworkProxy? ) { withBGApi { chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg -> - when (msg) { - is CR.RcvFileProgressXFTP -> { - state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl) - MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg)) + 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)) } - is CR.RcvStandaloneFileComplete -> { + 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) - MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg)) + state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy) + MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy)) } } - is CR.RcvFileError -> { + 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) + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy) } - is CR.ChatRespError -> { - if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) { + 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) + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy) } else { - Log.d(TAG, "unsupported error: ${msg.responseType}") + Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.err)}") } } else -> Log.d(TAG, "unsupported event: ${msg.responseType}") @@ -578,7 +621,7 @@ private fun MutableState.startDownloading( val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl) if (res == null) { - state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg) + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy) AlertManager.shared.showAlertMsg( generalGetString(MR.strings.migrate_to_device_error_downloading_archive), error @@ -587,30 +630,28 @@ private fun MutableState.startDownloading( } } -private fun MutableState.importArchive(archivePath: String, netCfg: NetCfg) { +private fun MutableState.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()) { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.chat_database_imported), - generalGetString(MR.strings.non_fatal_errors_occured_during_import) - ) + showArchiveImportedWithErrorsAlert(archiveErrors) } - state = MigrationToState.Passphrase("", netCfg) - MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg)) + state = MigrationToState.Passphrase("", netCfg, networkProxy) + MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg, networkProxy)) } catch (e: Exception) { - state = MigrationToState.ArchiveImportFailed(archivePath, netCfg) + 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) + state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy) AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString()) } } @@ -620,7 +661,7 @@ private suspend fun stopArchiveDownloading(fileId: Long, ctrl: ChatCtrl) { controller.apiCancelFile(null, fileId, ctrl) } -private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) { +private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) { if (useKeychain) { ksDatabasePassword.set(passphrase) } else { @@ -632,7 +673,8 @@ private fun startChat(passphrase: String, confirmation: MigrationConfirmation, u try { initChatController(useKey = passphrase, confirmMigrations = confirmation) { CompletableDeferred(false) } val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()).copy( - networkConfig = netCfg + networkConfig = netCfg, + networkProxy = networkProxy ) finishMigration(appSettings, close) } catch (e: Exception) { @@ -650,6 +692,7 @@ private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit) 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) { @@ -669,7 +712,7 @@ private suspend fun MutableState.cleanUpOnBack(chatReceiver: 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() - initChatControllerAndRunMigrations() + initChatControllerOnStart() } else if (state is MigrationToState.DownloadProgress && state.ctrl != null) { stopArchiveDownloading(state.fileId, state.ctrl) } @@ -697,7 +740,7 @@ private class MigrationToChatReceiver( val ctrl: ChatCtrl, val databaseUrl: File, var receiveMessages: Boolean = true, - val processReceivedMsg: suspend (CR) -> Unit + val processReceivedMsg: suspend (API) -> Unit ) { fun start() { Log.d(TAG, "MigrationChatReceiver startReceiver") @@ -706,19 +749,18 @@ private class MigrationToChatReceiver( try { val msg = ChatController.recvMsg(ctrl) if (msg != null && receiveMessages) { - val r = msg.resp - val rhId = msg.remoteHostId - Log.d(TAG, "processReceivedMsg: ${r.responseType}") - chatModel.addTerminalItem(TerminalItem.resp(rhId, r)) + val rhId = msg.rhId + Log.d(TAG, "processReceivedMsg: ${msg.responseType}") + chatModel.addTerminalItem(TerminalItem.resp(rhId, msg)) val finishedWithoutTimeout = withTimeoutOrNull(60_000L) { - processReceivedMsg(r) + processReceivedMsg(msg) } if (finishedWithoutTimeout == null) { - Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType) + 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.resp.responseType + "\n" + Exception().stackTraceToString()), + text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()), shareText = true ) } 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 da59050a3a..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 @@ -15,9 +15,7 @@ import chat.simplex.res.MR @Composable fun AddContactLearnMore(close: () -> Unit) { - ColumnWithScrollBar( - Modifier.padding(horizontal = DEFAULT_PADDING), - ) { + 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) 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 4c63d0a974..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 -> + 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,11 +99,7 @@ fun AddGroupLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { ModalView(close = close) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) Box( Modifier @@ -116,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 @@ -128,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( @@ -164,7 +167,6 @@ fun AddGroupLayout( } } } - } } 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/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 5b56fe5e39..330c80b7a2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -11,7 +11,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import java.net.URI +import kotlinx.coroutines.* enum class ConnectionLinkType { INVITATION, CONTACT, GROUP @@ -19,16 +19,27 @@ enum class ConnectionLinkType { suspend fun planAndConnect( rhId: Long?, - uri: URI, + shortOrFullLink: String, incognito: Boolean?, close: (() -> Unit)?, cleanup: (() -> Unit)? = null, filterKnownContact: ((Contact) -> Unit)? = null, filterKnownGroup: ((GroupInfo) -> Unit)? = null, -) { - val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString()) - if (connectionPlan != null) { - val link = strHasSingleSimplexLink(uri.toString().trim()) +): CompletableDeferred { + val completable = CompletableDeferred() + 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) "

${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}" else @@ -38,10 +49,10 @@ suspend fun planAndConnect( InvitationLinkPlan.Ok -> { Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito") if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) + 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 = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, connectDestructive = false, @@ -56,7 +67,7 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), 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 = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, onDismiss = cleanup, onDismissRequest = cleanup, destructive = true, @@ -64,7 +75,7 @@ suspend fun planAndConnect( ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, connectDestructive = true, @@ -85,7 +96,7 @@ suspend fun planAndConnect( String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } else { AlertManager.privacySensitive.showAlertMsg( @@ -93,7 +104,7 @@ suspend fun planAndConnect( generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } is InvitationLinkPlan.Known -> { @@ -108,7 +119,7 @@ suspend fun planAndConnect( String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } } @@ -116,10 +127,10 @@ suspend fun planAndConnect( ContactAddressPlan.Ok -> { Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito") if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) + 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 = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, connectDestructive = false, @@ -134,7 +145,7 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), 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 = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, destructive = true, onDismiss = cleanup, onDismissRequest = cleanup, @@ -142,7 +153,7 @@ suspend fun planAndConnect( ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, connectDestructive = true, @@ -157,7 +168,7 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_plan_repeat_connection_request), 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 = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, onDismiss = cleanup, onDismissRequest = cleanup, destructive = true, @@ -165,7 +176,7 @@ suspend fun planAndConnect( ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_repeat_connection_request), text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, connectDestructive = true, @@ -185,7 +196,7 @@ suspend fun planAndConnect( String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + cleanup() } } is ContactAddressPlan.Known -> { @@ -200,19 +211,19 @@ suspend fun planAndConnect( String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, hostDevice = hostDevice(rhId), ) - cleanup?.invoke() + 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?.invoke() + cleanup() } } is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { @@ -223,14 +234,14 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_via_group_link), 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 = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + 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 = generalGetString(MR.strings.you_will_join_group) + linkText, connectDestructive = false, @@ -244,7 +255,7 @@ suspend fun planAndConnect( if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - ownGroupLinkConfirmConnect(chatModel, rhId, uri, linkText, incognito, connectionPlan, groupInfo, close, cleanup) + ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, incognito, connectionPlan, groupInfo, close, cleanup) } } GroupLinkPlan.ConnectingConfirmReconnect -> { @@ -254,7 +265,7 @@ suspend fun planAndConnect( title = generalGetString(MR.strings.connect_plan_repeat_join_request), 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 = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, onDismiss = cleanup, onDismissRequest = cleanup, destructive = true, @@ -262,7 +273,7 @@ suspend fun planAndConnect( ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_repeat_join_request), text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, connectDestructive = true, @@ -274,10 +285,17 @@ 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) + linkText - ) + 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), @@ -285,7 +303,7 @@ suspend fun planAndConnect( hostDevice = hostDevice(rhId), ) } - cleanup?.invoke() + cleanup() } is GroupLinkPlan.Known -> { Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") @@ -294,44 +312,56 @@ suspend fun planAndConnect( filterKnownGroup(groupInfo) } else { 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) + linkText, - hostDevice = hostDevice(rhId), - ) - cleanup?.invoke() + 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() } } } - } - } else { - Log.d(TAG, "planAndConnect, plan error") - if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close, cleanup) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan = null, close, - title = generalGetString(MR.strings.connect_plan_connect_via_link), - connectDestructive = false, - cleanup = 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, + ) + } + } } } + return completable } suspend fun connectViaUri( chatModel: ChatModel, rhId: Long?, - uri: URI, + connLink: CreatedConnLink, incognito: Boolean, connectionPlan: ConnectionPlan?, close: (() -> Unit)?, cleanup: (() -> Unit)?, -) { - val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString()) - val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION +): 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), @@ -345,20 +375,22 @@ suspend fun connectViaUri( ) } 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, @@ -375,7 +407,7 @@ fun askCurrentOrIncognitoProfileAlert( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) @@ -383,7 +415,7 @@ fun askCurrentOrIncognitoProfileAlert( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) + 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) @@ -406,7 +438,7 @@ fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, co val c = chatModel.getContactChat(contact.contactId) if (c != null) { close?.invoke() - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } } @@ -414,7 +446,7 @@ 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?, @@ -440,7 +472,7 @@ fun ownGroupLinkConfirmConnect( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }) { Text( @@ -453,7 +485,7 @@ fun ownGroupLinkConfirmConnect( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) + 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) @@ -462,7 +494,7 @@ fun ownGroupLinkConfirmConnect( SectionItemView({ AlertManager.privacySensitive.hideAlert() withBGApi { - connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) + 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) @@ -487,7 +519,7 @@ fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, grou val g = chatModel.getGroupChat(groupInfo.groupId) if (g != null) { close?.invoke() - openGroupChat(rhId, groupInfo.groupId, chatModel) + openGroupChat(rhId, groupInfo.groupId) } } } 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 31623e4a61..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.* @@ -26,19 +26,20 @@ import chat.simplex.common.model.PendingContactConnection 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) { - if (connReqInvitation != null) { - chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false) + LaunchedEffect(connLinkInvitation) { + if (connLinkInvitation != null) { + chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connLinkInvitation, false, conn = contactConnection) } } /** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation]. @@ -53,16 +54,16 @@ fun ContactConnectionInfoView( } } } - 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.end.showModalCloseable { close -> AddContactLearnMore(close) @@ -74,13 +75,13 @@ fun ContactConnectionInfoView( @Composable private fun ContactConnectionInfoLayout( chatModel: ChatModel, - connReq: String?, + connLink: CreatedConnLink?, + showShortLink: MutableState, contactConnection: PendingContactConnection, focusAlias: Boolean, rhId: Long?, deleteConnection: () -> Unit, onLocalAliasChanged: (String) -> Unit, - share: () -> Unit, learnMore: () -> Unit, ) { @Composable fun incognitoEnabled() { @@ -88,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 ) { @@ -104,9 +105,7 @@ private fun ContactConnectionInfoLayout( } } - ColumnWithScrollBar( - Modifier, - ) { + ColumnWithScrollBar { AppBarTitle( stringResource( if (contactConnection.initiated) MR.strings.you_invited_a_contact @@ -128,20 +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) + 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) @@ -150,14 +155,14 @@ private fun ContactConnectionInfoLayout( } @Composable -fun ShareLinkButton(connReqInvitation: String) { +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(connReqInvitation)) + clipboard.shareText(simplexChatLink(linkUri)) }, iconColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary, @@ -186,7 +191,9 @@ fun DeleteButton(onClick: () -> Unit) { 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) + } } } @@ -200,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/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 26c5422623..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,168 +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, 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.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = 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.add_contact_tab, - MR.strings.create_group_button -) -private val icons = listOf(MR.images.ic_add_link, 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, contactTypes: List): List { + return c.filter { chat -> contactTypes.contains(chatContactType(chat)) } +} @Composable -private fun NewChatSheetLayout( - newChatSheetState: StateFlow, - stopped: Boolean, +private fun ModalData.NewChatSheetLayout( + rh: RemoteHostInfo?, addContact: () -> 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(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 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, 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, + searchShowingSimplexLink: MutableState, + searchChatFilteredBySimplexLink: MutableState, + 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, 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, + searchChatFilteredBySimplexLink: State, + searchText: String, + contactChats: List +): List { + 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 { 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, searchEmpty: Boolean): List { + 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(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, + ) + } } } } @@ -260,12 +830,6 @@ fun ActionButton( @Composable private fun PreviewNewChatSheet() { SimpleXTheme { - NewChatSheetLayout( - MutableStateFlow(AnimatedViewState.VISIBLE), - stopped = false, - addContact = {}, - 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 index f341d59305..1b3138d21c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -4,17 +4,23 @@ 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 @@ -24,14 +30,15 @@ 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.launch -import java.net.URI +import kotlinx.coroutines.* enum class NewChatOption { INVITE, CONNECT @@ -41,18 +48,18 @@ enum class NewChatOption { 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 = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) } - val connReqInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connReq ?: "" } } + val contactConnection: MutableState = 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 - && connReqInvitation.isEmpty() + && connLinkInvitation.connFullLink.isEmpty() && contactConnection.value == null && !creatingConnReq.value ) { - createInvitation(rh?.remoteHostId, creatingConnReq, connReqInvitation, contactConnection) + createInvitation(rh?.remoteHostId, creatingConnReq, connLinkInvitation, contactConnection) } } DisposableEffect(Unit) { @@ -60,9 +67,8 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC /** 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] **/ - if (chatModel.showingInvitation.value != null && (!ModalManager.center.hasModalsOpen() || appPlatform.isDesktop)) { + if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() <= 1) { val conn = contactConnection.value if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) { AlertManager.shared.showAlertDialog( @@ -77,7 +83,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC controller.deleteChat(Chat(remoteHostId = rh?.remoteHostId, chatInfo = chatInfo, chatItems = listOf())) if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null - ModalManager.end.closeModals() + ModalManager.start.closeModals() } } } @@ -90,89 +96,84 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC val tabTitles = NewChatOption.values().map { when(it) { NewChatOption.INVITE -> - stringResource(MR.strings.add_contact_tab) + stringResource(MR.strings.one_time_link_short) NewChatOption.CONNECT -> stringResource(MR.strings.connect_via_link) } } - Column( - Modifier.fillMaxSize(), - ) { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = bottomPadding) - Column(Modifier.align(Alignment.CenterEnd).padding(bottom = bottomPadding, end = DEFAULT_PADDING)) { - AddContactLearnMoreButton() + 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] } - } - 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.fillMaxSize(), verticalAlignment = Alignment.Top) { index -> - // LALAL SCROLLBAR DOESN'T WORK - ColumnWithScrollBar( - Modifier - .fillMaxSize(), - verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connReqInvitation.isEmpty()) Arrangement.Center else Arrangement.Top) { - Spacer(Modifier.height(DEFAULT_PADDING)) - when (index) { - NewChatOption.INVITE.ordinal -> { - PrepareAndInviteView(rh?.remoteHostId, contactConnection, connReqInvitation, creatingConnReq) - } - NewChatOption.CONNECT.ordinal -> { - ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) - } + 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() } - SectionBottomSpacer() } } } } @Composable -private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState, connReqInvitation: String, creatingConnReq: MutableState) { - if (connReqInvitation.isNotEmpty()) { +private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState) { + if (connLinkInvitation.connFullLink.isNotEmpty()) { InviteView( rhId, - connReqInvitation = connReqInvitation, + connLinkInvitation = connLinkInvitation, contactConnection = contactConnection, ) } else if (creatingConnReq.value) { CreatingLinkProgressView() } else { - RetryButton { createInvitation(rhId, creatingConnReq, connReqInvitation, contactConnection) } + RetryButton { createInvitation(rhId, creatingConnReq, connLinkInvitation, contactConnection) } } } @@ -181,6 +182,15 @@ 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( @@ -197,41 +207,351 @@ private fun RetryButton(onClick: () -> Unit) { } @Composable -private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState) { - SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase()) { - LinkTextView(connReqInvitation, true) - } - - Spacer(Modifier.height(10.dp)) - - SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase()) { - SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() }) - } - - Spacer(Modifier.height(10.dp)) - val incognito = remember { mutableStateOf(controller.appPrefs.incognito.get()) } - IncognitoToggle(controller.appPrefs.incognito, incognito) { - if (appPlatform.isDesktop) ModalManager.end.closeModals() - ModalManager.end.showModal { IncognitoView() } - } - KeyChangeEffect(incognito.value) { - withBGApi { - val contactConn = contactConnection.value ?: return@withBGApi - val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi - contactConnection.value = conn - chatModel.updateContactConnection(rhId, conn) +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, + ) } - chatModel.markShowingInvitationUsed() } - SectionTextFooter(sharedProfileInfo(chatModel, incognito.value)) + Divider( + Modifier.padding( + start = DEFAULT_PADDING_HALF, + end = DEFAULT_PADDING_HALF, + ) + ) } @Composable -private fun AddContactLearnMoreButton() { +fun ActiveProfilePicker( + search: MutableState, + 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) { + 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) { + 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( { - if (appPlatform.isDesktop) ModalManager.end.closeModals() - ModalManager.end.showModalCloseable { close -> + ModalManager.start.showModalCloseable { close -> AddContactLearnMore(close) } } @@ -239,30 +559,30 @@ private fun AddContactLearnMoreButton() { Icon( painterResource(MR.images.ic_info), stringResource(MR.strings.learn_more), + tint = MaterialTheme.colors.primary ) } } @Composable private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, pastedLink: MutableState, close: () -> Unit) { - SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase()) { + 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()) { + SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) { QRCodeScanner(showQRCodeScanner) { text -> - withBGApi { - val res = verify(rhId, text, close) - if (!res) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_qr_code), - text = generalGetString(MR.strings.code_you_scanned_is_not_simplex_link_qr_code) - ) - } + 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) } } } @@ -296,7 +616,7 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState, showQRC @Composable fun LinkTextView(link: String, share: Boolean) { val clipboard = LocalClipboardManager.current - Row(Modifier.fillMaxWidth().heightIn(min = 46.dp).padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + 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) @@ -320,6 +640,7 @@ fun LinkTextView(link: String, share: Boolean) { enabled = false, isError = false, interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) }) } @@ -338,38 +659,54 @@ fun LinkTextView(link: String, share: Boolean) { } } -private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boolean { +private fun filteredProfiles(users: List, searchTextOrPassword: String): List { + 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)) { - connect(rhId, text, close) - return true + return withContext(Dispatchers.Default) { + connect(rhId, text, close) + } } return false } -private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null) { +private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null): Boolean = planAndConnect( rhId, - URI.create(link), + link, close = close, cleanup = cleanup, incognito = null - ) -} + ).await() private fun createInvitation( rhId: Long?, creatingConnReq: MutableState, - connReqInvitation: String, + connLinkInvitation: CreatedConnLink, contactConnection: MutableState ) { - if (connReqInvitation.isNotEmpty() || contactConnection.value != null || creatingConnReq.value) return + 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) { - chatModel.updateContactConnection(rhId, r.second) - chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false) - contactConnection.value = r.second + 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) { 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 e38c983487..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,13 +12,33 @@ 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.* 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, @@ -38,14 +58,6 @@ fun SimpleXLinkQRCode( ) } -fun simplexChatLink(uri: String): String { - return if (uri.startsWith("simplex:/")) { - uri.replace("simplex:/", "https://simplex.chat/") - } else { - uri - } -} - @Composable fun QRCode( connReq: String, 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 1e497e0581..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 @@ -10,5 +10,5 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF expect fun QRCodeScanner( showQRCodeScanner: MutableState = remember { mutableStateOf(true) }, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), - onBarcode: (String) -> Unit + 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>, + selectedOperatorIds: MutableState>, + 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>) { + 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>, + selectedOperatorIds: State> +) { + fun continueOnAccept() { + if (appPlatform.isDesktop) { + continueToNextStep() + } else { + continueToSetNotificationsAfterAccept() + } + } + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.onboarding_conditions_accept, + onboarding = null, + enabled = enabled, + onclick = { + withBGApi { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + val 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, selectedOperatorIds: Set): List? { + 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 5e50475951..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ /dev/null @@ -1,179 +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 = { - withBGApi { - 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, -) { - ColumnWithScrollBar( - Modifier.fillMaxSize().padding(top = DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarTitle(stringResource(MR.strings.simplex_address)) - - Spacer(Modifier.weight(1f)) - - if (userAddress != null) { - SimpleXLinkQRCode(userAddress.connReqContact) - 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?) { - // 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(rhId) ?: return@withBGApi - chatModel.currentUser.value = user - chatModel.controller.startChat(user) - } -} 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 28fa1d3a48..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.* @@ -23,35 +24,30 @@ import dev.icerock.moko.resources.StringResource @Composable fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { - ColumnWithScrollBar(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 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, 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 9124808959..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.ColumnWithScrollBar +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) { - ColumnWithScrollBar( - modifier = Modifier - .fillMaxSize() - .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, mode: NotificationsMode, title: StringResource, description: StringResource) { +fun SelectableCard(currentValue: State, 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, 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 65bd89b11b..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) } @@ -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, false) + 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, ) { - ColumnWithScrollBar( - Modifier.fillMaxSize().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 84c48bbc1b..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,90 +1,90 @@ 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.* 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.platform.ColumnWithScrollBar -import chat.simplex.common.platform.chatModel +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?, - showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + onboardingStage: SharedPreference? ) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .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(top = DEFAULT_PADDING), contentAlignment = Alignment.Center - ) { - SimpleButtonDecorated(text = stringResource(MR.strings.migrate_from_another_device), icon = painterResource(MR.images.ic_download), - click = { - chatModel.migrationState.value = MigrationToState.PasteOrScanLink - ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) } }) - } - } - - Box( - Modifier - .fillMaxWidth() - .padding(bottom = DEFAULT_PADDING.times(1.5f), top = if (onboardingStage == null) DEFAULT_PADDING else 0.dp), contentAlignment = Alignment.Center - ) { - SimpleButtonDecorated(text = stringResource(MR.strings.how_it_works), icon = painterResource(MR.images.ic_info), - click = showModal { HowItWorks(user, onboardingStage) }) } } LaunchedEffect(Unit) { @@ -99,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) } } } @@ -123,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 + ) + } } } @@ -168,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 df6c66deb4..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,29 +8,46 @@ 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.platform.ColumnWithScrollBar +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>) { @Composable fun linkButton(link: String) { val uriHandler = LocalUriHandler.current @@ -47,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) + } + } } } @@ -108,23 +136,49 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { ModalView(close = close) { ColumnWithScrollBar( Modifier - .fillMaxSize() .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> = 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, + val features: List, val post: String? = null, ) @@ -167,18 +259,18 @@ private val versionDescriptions: List = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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,27 +522,27 @@ private val versionDescriptions: List = 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 @@ -466,32 +553,31 @@ private val versionDescriptions: List = listOf( version = "v5.5", post = "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_folder_pen, titleId = MR.strings.v5_5_private_notes, descrId = MR.strings.v5_5_private_notes_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_link, titleId = MR.strings.v5_5_simpler_connect_ui, descrId = MR.strings.v5_5_simpler_connect_ui_descr ), - FeatureDescription( + 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" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_battery_3_bar, titleId = MR.strings.v5_5_message_delivery, descrId = MR.strings.v5_5_message_delivery_descr ), - FeatureDescription( + 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, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -499,22 +585,22 @@ private val versionDescriptions: List = listOf( 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( - FeatureDescription( + 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 ), - FeatureDescription( + 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 ), - FeatureDescription( + 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 ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_back_hand, titleId = MR.strings.v5_6_safer_groups, descrId = MR.strings.v5_6_safer_groups_descr @@ -525,45 +611,221 @@ private val versionDescriptions: List = listOf( version = "v5.7", post = "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html", features = listOf( - FeatureDescription( + 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 ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_forward, titleId = MR.strings.v5_7_forward, descrId = MR.strings.v5_7_forward_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_music_note, titleId = MR.strings.v5_7_call_sounds, descrId = MR.strings.v5_7_call_sounds_descr ), - FeatureDescription( + 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 ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_wifi_tethering, titleId = MR.strings.v5_7_network, descrId = MR.strings.v5_7_network_descr ), - FeatureDescription( + 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, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), + 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 { @@ -580,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 76f522c614..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() } val session = remember { chatModel.remoteCtrlSession }.value - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + 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) { 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) @@ -341,7 +358,7 @@ private fun ScanDesktopAddressView(sessionAddress: MutableState) { SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) { QRCodeScanner { text -> sessionAddress.value = text - processDesktopQRCode(sessionAddress, text) + connectDesktopAddress(sessionAddress, text) } } } @@ -383,7 +400,7 @@ private fun DesktopAddressView(sessionAddress: MutableState) { stringResource(MR.strings.connect_to_desktop), disabled = sessionAddress.value.isEmpty(), click = { - connectDesktopAddress(sessionAddress, sessionAddress.value) + withBGApi { connectDesktopAddress(sessionAddress, sessionAddress.value) } }, ) } @@ -391,9 +408,7 @@ private fun DesktopAddressView(sessionAddress: MutableState) { @Composable private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.linked_desktops)) SectionView(stringResource(MR.strings.desktop_devices).uppercase()) { remoteCtrls.forEach { rc -> @@ -413,14 +428,14 @@ private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList) { 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) } } @@ -448,10 +463,6 @@ private suspend fun updateRemoteCtrls(remoteCtrls: SnapshotStateList, resp: String) { - connectDesktopAddress(sessionAddress, resp) -} - private fun findKnownDesktop(showConnectScreen: MutableState) { withBGApi { if (controller.findKnownRemoteCtrl()) { @@ -465,45 +476,48 @@ private fun findKnownDesktop(showConnectScreen: MutableState) { } } -private fun confirmKnownDesktop(sessionAddress: MutableState, rc: RemoteCtrlInfo) { - connectDesktop(sessionAddress) { - controller.confirmRemoteCtrl(rc.remoteCtrlId) +private suspend fun confirmKnownDesktop(sessionAddress: MutableState, rc: RemoteCtrlInfo): Boolean { + return withContext(Dispatchers.Default) { + connectDesktop(sessionAddress) { + controller.confirmRemoteCtrl(rc.remoteCtrlId) + } } } -private fun connectDesktopAddress(sessionAddress: MutableState, addr: String) { - connectDesktop(sessionAddress) { - controller.connectRemoteCtrl(addr) +private suspend fun connectDesktopAddress(sessionAddress: MutableState, addr: String): Boolean { + return withContext(Dispatchers.Default) { + connectDesktop(sessionAddress) { + controller.connectRemoteCtrl(addr) + } } } -private fun connectDesktop(sessionAddress: MutableState, connect: suspend () -> Pair) { - 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, connect: suspend () -> Pair): 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, 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 e13b86258d..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 @@ -89,18 +90,15 @@ fun ConnectMobileLayout( connectDesktop: () -> Unit, deleteHost: (RemoteHostInfo) -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - 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) { @@ -179,10 +177,15 @@ private fun ConnectMobileViewLayout( refreshQrCode: () -> Unit = {}, UnderQrLayout: @Composable () -> Unit = {}, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - 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) } 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 9b6e3a0937..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ /dev/null @@ -1,446 +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.common.platform.ColumnWithScrollBar -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) } - var 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 - val networkTCPKeepIntvl: MutableState - val networkTCPKeepCnt: MutableState - 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, - rcvConcurrency = networkRcvConcurrency.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 - 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) { - withBGApi { - 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, - networkRcvConcurrency, - 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, - networkTCPTimeout: MutableState, - networkTCPTimeoutPerKb: MutableState, - networkRcvConcurrency: MutableState, - networkSMPPingInterval: MutableState, - networkSMPPingCount: MutableState, - networkEnableKeepAlive: MutableState, - networkTCPKeepIdle: MutableState, - networkTCPKeepIntvl: MutableState, - networkTCPKeepCnt: MutableState, - resetDisabled: Boolean, - reset: () -> Unit, - footerDisabled: Boolean, - revert: () -> Unit, - save: () -> Unit -) { - val secondsLabel = stringResource(MR.strings.network_option_seconds_label) - - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { - 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(10_000000, 15_000000, 20_000000, 25_000000, 35_000000, 50_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) - } - } - } - 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 -) { - 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, values: List, 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, values: List, 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), - contentColor = LocalContentColor.current - ) { - 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) }, - 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) }, - 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 78e24e5e7e..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,42 +1,64 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionView -import androidx.compose.foundation.Image +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.clip +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* -import androidx.compose.ui.layout.ContentScale +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 ProfileImageSection() { - SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + 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 @@ -55,6 +77,7 @@ object AppearanceScope { 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, @@ -66,90 +89,605 @@ object AppearanceScope { } @Composable - fun ThemesSection( - systemDarkTheme: SharedPreference, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit - ) { - val currentTheme by CurrentColors.collectAsState() - SectionView(stringResource(MR.strings.settings_section_title_themes)) { - val darkTheme = isSystemInDarkTheme() - val state = remember { derivedStateOf { currentTheme.name } } - ThemeSelector(state) { - ThemeManager.applyTheme(it, darkTheme) - } - if (state.value == DefaultTheme.SYSTEM.name) { - DarkThemeSelector(remember { systemDarkTheme.state }) { - ThemeManager.changeDarkTheme(it, darkTheme) + 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, + ) + ) + } } } } - SectionItemView(showSettingsModal { _ -> CustomizeThemeView(editColor) }) { Text(stringResource(MR.strings.customize_theme_title)) } } @Composable - fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), + 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) { + val currentTheme by CurrentColors.collectAsState() + val baseTheme = currentTheme.base + val wallpaperType = MaterialTheme.wallpaper.type + val themeUserDestination: MutableState?> = 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 = 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 = 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? -> @@ -161,9 +699,11 @@ object AppearanceScope { } } SectionItemView({ - val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme) - theme.value = yaml.encodeToString(overrides) - withLongRunningApi { exportThemeLauncher.launch("simplex.theme")} + val overrides = ThemeManager.currentThemeOverridesForExport(null, null/*chatModel.currentUser.value?.uiThemes*/) + val lines = yaml.encodeToString(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) } @@ -171,7 +711,8 @@ object AppearanceScope { if (to != null) { val theme = getThemeFromUri(to) if (theme != null) { - ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme) + ThemeManager.saveAndApplyThemeOverrides(theme) + saveThemeToDatabase(null) } } } @@ -185,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?) { + 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?>) { + 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, onSelected: (String) -> Unit) { // Should be the same as in app/build.gradle's `android.defaultConfig.resConfigs` @@ -235,9 +1096,11 @@ 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", @@ -254,7 +1117,7 @@ object AppearanceScope { "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, @@ -266,13 +1129,18 @@ object AppearanceScope { } @Composable - private fun ThemeSelector(state: State, 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, 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, @@ -282,15 +1150,16 @@ object AppearanceScope { } @Composable - private fun DarkThemeSelector(state: State, onSelected: (String) -> Unit) { + private fun DarkModeThemeSelector(state: State, onSelected: (String) -> Unit) { val values by remember { val darkThemes = ArrayList>() - 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, @@ -303,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?>, perUserTheme: MutableState) { + 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 94415b0ee0..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 @@ -36,10 +36,7 @@ fun CallSettingsLayout( callOnLockScreen: SharedPreference, editIceServers: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - 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 ef8c30b43a..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,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionDividerSpaced import SectionSpacer import SectionTextFooter import SectionView @@ -9,9 +10,11 @@ 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 @@ -20,19 +23,19 @@ import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable -fun DeveloperView( - m: ChatModel, - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit +fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + 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 } + 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) }) } + 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) SectionTextFooter( generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " + @@ -40,8 +43,14 @@ fun DeveloperView( ) } if (devTools.value) { - SectionSpacer() + 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 -> 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 c2bf69bc0e..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 @@ -21,11 +21,7 @@ fun HelpView(userDisplayName: String) { @Composable fun HelpLayout(userDisplayName: String) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - .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 4dd406c398..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 @@ -18,6 +16,7 @@ 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 @@ -36,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) { @@ -54,12 +56,9 @@ private fun HiddenProfileLayout( user: User, saveProfilePassword: (String) -> Unit ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + 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/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt deleted file mode 100644 index 4d33040e29..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ /dev/null @@ -1,513 +0,0 @@ -package chat.simplex.common.views.usersettings - -import SectionBottomSpacer -import SectionCustomFooter -import SectionDividerSpaced -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.graphics.Color -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.* -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() { - 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 = remember { mutableStateOf(netCfg.useSocksProxy) } - val developerTools = chatModel.controller.appPrefs.developerTools.get() - val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } - val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } - - 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, - 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 = { - withBGApi { - 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 = { - withBGApi { - 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 - } - ) { - withBGApi { - 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 } - ) { - withBGApi { - 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, - onionHosts: MutableState, - sessionMode: MutableState, - proxyPort: State, - toggleSocksProxy: (Boolean) -> Unit, - useOnion: (OnionHosts) -> Unit, - updateSessionMode: (TransportSessionMode) -> Unit, -) { - val m = chatModel - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - 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), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } }) - - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } }) - - if (currentRemoteHost == null) { - val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } - UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false) - UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) - if (developerTools) { - SessionModePicker(sessionMode, showModal, updateSessionMode) - } - SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showModal { AdvancedNetworkSettingsView(m) } }) - } - } - } - 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), { 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, - onionHosts: MutableState, - sessionMode: MutableState, - networkProxyHostPort: SharedPreference, - proxyPort: State, - toggleSocksProxy: (Boolean) -> Unit, - useOnion: (OnionHosts) -> Unit, - updateSessionMode: (TransportSessionMode) -> Unit, -) { - val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) } - UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, networkProxyHostPort, true) - UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) - if (developerTools) { - SessionModePicker(sessionMode, showModal, updateSessionMode) - } -} - -@Composable -fun UseSocksProxySwitch( - networkUseSocksProxy: MutableState, - proxyPort: State, - toggleSocksProxy: (Boolean) -> Unit, - showModal: (@Composable ModalData.() -> Unit) -> Unit, - networkProxyHostPort: SharedPreference = chatModel.controller.appPrefs.networkProxyHostPort, - migration: Boolean = false, -) { - 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) - val disabledStyle = SpanStyle(color = MaterialTheme.colors.onBackground) - withAnnotation(tag = "PORT", annotation = generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) { - withStyle(if (networkUseSocksProxy.value || !migration) style else disabledStyle) { - 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 { _ -> - if (networkUseSocksProxy.value || !migration) { - showModal { SockProxySettings(chatModel, networkProxyHostPort, migration) } - } - } - }, - shouldConsumeEvent = { offset -> - text.getStringAnnotations(tag = "PORT", start = offset, end = offset).any() - } - ) - } - DefaultSwitch( - checked = networkUseSocksProxy.value, - onCheckedChange = toggleSocksProxy, - ) - } -} - -@Composable -fun SockProxySettings( - m: ChatModel, - networkProxyHostPort: SharedPreference = m.controller.appPrefs.networkProxyHostPort, - migration: Boolean, -) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { - val defaultHostPort = remember { "localhost:9050" } - AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) - val hostPortSaved by remember { networkProxyHostPort.state } - val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost")) - } - val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050")) - } - val save = { - withBGApi { - networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text) - if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) { - m.controller.apiSetNetworkConfig(m.controller.getNetCfg()) - } - } - } - SectionView { - SectionItemView { - ResetToDefaultsButton({ - val reset = { - 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() && !migration) { - showUpdateNetworkSettingsDialog { - reset() - } - } else { - reset() - } - }, disabled = hostPortSaved == 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 = hostPortSaved?.split(":")?.firstOrNull() ?: "localhost" - val prevPort = hostPortSaved?.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() && !migration) showUpdateNetworkSettingsDialog { save() } else save() }, - revertDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text), - saveDisabled = hostPortSaved == (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, - enabled: State, - showModal: (@Composable ModalData.() -> 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 { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { - AppBarTitle(stringResource(MR.strings.network_use_onion_hosts)) - SectionViewSelectable(null, onionHosts, values, useOnion) - } - } - } - - if (enabled.value) { - SectionItemWithValue( - generalGetString(MR.strings.network_use_onion_hosts), - onionHosts, - values, - icon = painterResource(MR.images.ic_security), - enabled = enabled, - onSelected = onSelected - ) - } else { - // In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before - SectionItemWithValue( - generalGetString(MR.strings.network_use_onion_hosts), - remember { mutableStateOf(OnionHosts.NEVER) }, - listOf(ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc)))), - icon = painterResource(MR.images.ic_security), - enabled = enabled, - onSelected = {} - ) - } -} - -@Composable -private fun SessionModePicker( - sessionMode: MutableState, - showModal: (@Composable ModalData.() -> 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 { - ColumnWithScrollBar( - 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) }, - 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 515d73a426..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() } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + 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() } - ColumnWithScrollBar( - 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() } - ColumnWithScrollBar( - 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 cd0e40f5d0..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,12 +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,) { @@ -33,7 +34,9 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { 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() @@ -63,9 +66,7 @@ private fun PreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_preferences)) val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) } TimedMessagesFeatureSection(timedMessages) { @@ -122,9 +123,9 @@ private fun TimedMessagesFeatureSection(allowFeature: State, 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 b376285259..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.* @@ -31,6 +31,7 @@ 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.* +import kotlinx.coroutines.* enum class LAMode { SYSTEM, @@ -54,20 +55,17 @@ fun PrivacySettingsView( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), setPerformLA: (Boolean) -> Unit ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + 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), @@ -90,7 +88,33 @@ 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) { @@ -102,14 +126,16 @@ fun PrivacySettingsView( 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) + } } } } @@ -124,15 +150,17 @@ fun PrivacySettingsView( 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) + } } } } @@ -141,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) @@ -160,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) @@ -181,6 +209,26 @@ fun PrivacySettingsView( } } +@Composable +private fun ChatListLinksOptions(state: State, onSelected: (PrivacyChatListOpenLinksMode) -> Unit) { + val values = remember { + PrivacyChatListOpenLinksMode.entries.map { + when (it) { + PrivacyChatListOpenLinksMode.YES -> it to generalGetString(MR.strings.privacy_chat_list_open_links_yes) + PrivacyChatListOpenLinksMode.NO -> it to generalGetString(MR.strings.privacy_chat_list_open_links_no) + PrivacyChatListOpenLinksMode.ASK -> it to generalGetString(MR.strings.privacy_chat_list_open_links_ask) + } + } + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.privacy_chat_list_open_links), + values, + state, + icon = painterResource(MR.images.ic_open_in_new), + onSelected = onSelected + ) +} + @Composable private fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) { val modeValues = listOf(SimplexLinkMode.DESCRIPTION, SimplexLinkMode.FULL) @@ -204,6 +252,30 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State, onS ) } +@Composable +private fun BlurRadiusOptions(state: State, 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), @@ -219,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) } @@ -227,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) } @@ -326,7 +398,8 @@ fun SimplexLockView( currentLAMode: SharedPreference, 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 } } @@ -334,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() } @@ -357,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 -> { @@ -367,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) @@ -382,7 +452,7 @@ fun SimplexLockView( } LAMode.PASSCODE -> { ModalManager.fullscreen.showCustomModal { close -> - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( submit = { laLockDelay.set(30) @@ -403,7 +473,7 @@ fun SimplexLockView( } fun toggleSelfDestruct(selfDestruct: SharedPreference) { - 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 */ } @@ -422,11 +492,11 @@ 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, contentColor = LocalContentColor.current) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( reason = generalGetString(MR.strings.la_app_passcode), submit = { @@ -446,11 +516,11 @@ 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, contentColor = LocalContentColor.current) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( passcodeKeychain = ksSelfDestructPassword, prohibitedPasscodeKeychain = ksAppPassword, @@ -472,13 +542,11 @@ fun SimplexLockView( } } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + 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) { @@ -487,7 +555,7 @@ fun SimplexLockView( } LAMode.PASSCODE -> { ModalManager.fullscreen.showCustomModal { close -> - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( submit = { laLockDelay.set(30) @@ -495,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 ) @@ -598,7 +668,7 @@ private fun EnableSelfDestruct( selfDestruct: SharedPreference, close: () -> Unit ) { - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( passcodeKeychain = ksSelfDestructPassword, prohibitedPasscodeKeychain = ksAppPassword, title = generalGetString(MR.strings.set_passcode), reason = generalGetString(MR.strings.enabled_self_destruct_passcode), submit = { @@ -612,7 +682,7 @@ private fun EnableSelfDestruct( } @Composable -private fun EnableLock(performLA: MutableState, onCheckedChange: (Boolean) -> Unit) { +private fun EnableLock(performLA: State, 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 399895870f..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt +++ /dev/null @@ -1,204 +0,0 @@ -package chat.simplex.common.views.usersettings - -import SectionBottomSpacer -import SectionDividerSpaced -import SectionItemView -import SectionItemViewSpaceBetween -import SectionView -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.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.res.MR -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.distinctUntilChanged - -@Composable -fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) { - var testing by remember { mutableStateOf(false) } - ProtocolServerLayout( - testing, - server, - serverProtocol, - testServer = { - testing = true - withLongRunningApi { - 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, -) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { - 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() } - 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) - } - } -} - -@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 = - 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 fe847432fb..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ /dev/null @@ -1,366 +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.ColumnWithScrollBar -import chat.simplex.common.platform.appPlatform -import chat.simplex.res.MR - -@Composable -fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { - var presetServers by remember(rhId) { mutableStateOf(emptyList()) } - var servers by remember { stateGetOrPut("servers") { emptyList() } } - var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } } - val currServers = remember(rhId) { mutableStateOf(servers) } - val testing = rememberSaveable(rhId) { mutableStateOf(false) } - val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } } - val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } } - val saveDisabled = remember(servers) { - derivedStateOf { - servers.isEmpty() || - servers == currServers.value || - testing.value || - servers.none { srv -> - val address = parseServerAddress(srv.server) - address != null && uniqueAddress(srv, address, servers) - } || - allServersDisabled.value - } - } - - KeyChangeEffect(rhId) { - servers = emptyList() - serversAlreadyLoaded = false - } - - LaunchedEffect(rhId) { - withApi { - val res = m.controller.getUserProtoServers(rhId, serverProtocol) - if (res != null) { - currServers.value = res.protoServers - presetServers = res.presetServers - if (servers.isEmpty() && !serversAlreadyLoaded) { - servers = currServers.value - serversAlreadyLoaded = true - } - } - } - } - val testServersJob = CancellableOnGoneJob() - 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 - }, - onDelete = { - val newServers = ArrayList(servers) - newServers.removeAt(index) - servers = newServers - close() - }) - } - } - 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 - } - } - } - ) { - 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 = { - testServersJob.value = withLongRunningApi { - testServers(testing, servers, m) { - servers = it - } - } - }, - resetServers = { - servers = currServers.value - }, - 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, - serversUnchanged: Boolean, - saveDisabled: Boolean, - allServersDisabled: Boolean, - currentUser: User?, - addServer: () -> Unit, - testServers: () -> Unit, - resetServers: () -> Unit, - saveSMPServers: () -> Unit, - showServer: (ServerCfg) -> Unit, -) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { - 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, 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): Boolean = servers.all { srv -> - address.hostnames.all { host -> - srv.id == s.id || !srv.server.contains(host) - } -} - -private fun hasAllPresets(presetServers: List, servers: List, m: ChatModel): Boolean = - presetServers.all { hasPreset(it, servers) } ?: true - -private fun addAllPresets(rhId: Long?, presetServers: List, servers: List, m: ChatModel): List { - val toAdd = ArrayList() - 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): Boolean = - servers.any { it.server == srv } - -private suspend fun testServers(testing: MutableState, servers: List, m: ChatModel, onUpdated: (List) -> 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): List { - 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, m: ChatModel, onUpdated: (List) -> Unit): Map { - val fs: MutableMap = mutableMapOf() - val updatedServers = ArrayList(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 -} - -private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState>, servers: List, m: ChatModel, afterSave: () -> Unit = {}) { - withBGApi { - if (m.controller.setUserProtoServers(rhId, protocol, servers)) { - currServers.value = servers - } - 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/SetDeliveryReceiptsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt index f0960c1511..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 @@ -73,33 +73,24 @@ private fun SetDeliveryReceiptsLayout( skip: () -> Unit, userCount: Int, ) { - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).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)) + Spacer(Modifier.weight(1f)) - SkipButton(skip) + SkipButton(skip) - SectionBottomSpacer() - } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize().padding(end = endPadding)) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) + 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 298eb39737..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,26 +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.* @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, @@ -55,9 +52,9 @@ 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) }) } }, @@ -71,8 +68,10 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt } }, withAuth = ::doWithAuth, - drawerState = drawerState, ) + KeyChangeEffect(chatModel.updatingProgress.value != null) { + close() + } } val simplexTeamUri = @@ -80,7 +79,6 @@ val simplexTeamUri = @Composable fun SettingsLayout( - profile: LocalProfile?, stopped: Boolean, encrypted: Boolean, passphraseSaved: Boolean, @@ -93,118 +91,77 @@ fun SettingsLayout( 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()) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .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, drawerState) } } }, 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) - } - 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, 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() }, 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 ModalData.(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) { @@ -228,8 +185,7 @@ expect fun SettingsSectionApp( PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close) }() }), - disabled = stopped, - extraPadding = true + disabled = stopped ) } @@ -238,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") } } @@ -274,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) } } @@ -304,7 +259,7 @@ fun ChatLockItem( } @Composable fun TerminalAlwaysVisibleItem(pref: SharedPreference, 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, @@ -324,9 +279,34 @@ fun ChatLockItem( } } +@Composable fun ResetHintsItem(unchangedHints: MutableState) { + 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() { @@ -413,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 ) } } @@ -429,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 = {}, @@ -439,6 +422,7 @@ fun PreferenceToggleWithIcon( onCheckedChange = { onChange(it) }, + enabled = !disabled ) } } @@ -463,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) + } + ) + } } } } @@ -483,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) } @@ -498,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, @@ -511,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 63baece5cf..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,26 +1,94 @@ package chat.simplex.common.views.usersettings 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() { - ColumnWithScrollBar(Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING) - ) { - 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 b595bd4e0e..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,6 +5,7 @@ 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.shape.RoundedCornerShape @@ -31,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() @@ -58,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 @@ -66,27 +94,7 @@ fun UserAddressView( user = user.value, userAddress = userAddress.value, shareViaProfile, - onCloseHandler, - createAddress = { - withBGApi { - 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 { UserAddressLearnMore() @@ -96,7 +104,7 @@ fun UserAddressView( 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, @@ -132,12 +140,8 @@ fun UserAddressView( ) } - if (viaCreateLinkView) { + ModalView(close = close) { showLayout() - } else { - ModalView(close = { onCloseHandler.value(close) }) { - showLayout() - } } if (progressIndicator) { @@ -164,7 +168,6 @@ private fun UserAddressLayout( user: User?, userAddress: UserContactLinkRec?, shareViaProfile: MutableState, - onCloseHandler: MutableState<(close: () -> Unit) -> Unit>, createAddress: () -> Unit, learnMore: () -> Unit, share: (String) -> Unit, @@ -174,52 +177,60 @@ private fun UserAddressLayout( saveAas: (AutoAcceptState, MutableState) -> Unit, ) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId), withPadding = false) + 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) - 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() @@ -237,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, ) } @@ -257,6 +284,85 @@ fun ShareViaEmailButton(onClick: () -> Unit) { ) } +@Composable +private fun AddressSettingsButton( + user: User?, + userAddress: UserContactLinkRec, + shareViaProfile: MutableState, + setProfileAddress: (Boolean) -> Unit, + saveAas: (AutoAcceptState, MutableState) -> 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, + setProfileAddress: (Boolean) -> Unit, + saveAas: (AutoAcceptState, MutableState) -> 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, setProfileAddress: (Boolean) -> Unit) { PreferenceToggleWithIcon( @@ -293,17 +399,37 @@ fun ShareWithContactsButton(shareViaProfile: MutableState, setProfileAd onDismissRequest = { shareViaProfile.value = !on }) + } } +} + +@Composable +private fun BusinessAddressToggle(autoAcceptState: MutableState, 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, 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) } } @@ -324,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 } @@ -337,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 { @@ -353,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 } @@ -378,7 +509,9 @@ private fun AutoAcceptSection( saveAas: (AutoAcceptState, MutableState) -> 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) } } @@ -390,9 +523,9 @@ private fun AcceptIncognitoToggle(autoAcceptState: MutableState 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) } } @@ -402,7 +535,7 @@ private fun WelcomeMessageEditor(autoAcceptState: MutableState) 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) } } } @@ -432,7 +565,6 @@ fun PreviewUserAddressLayoutNoAddress() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, - onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) } @@ -458,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 = {}, @@ -466,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 e3636ec9c5..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,6 +34,7 @@ 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( @@ -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() @@ -102,7 +100,7 @@ fun UserProfileLayout( Modifier .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 @@ -169,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 b40cc7db92..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.* @@ -36,11 +36,10 @@ import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* @Composable -fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: MutableState, drawerState: DrawerState) { +fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: MutableState, 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) } } - val scope = rememberCoroutineScope() UserProfilesLayout( users = users, filteredUsers = filteredUsers, @@ -49,13 +48,9 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice, visibleUsersCount = visibleUsersCount(m), addUser = { - ModalManager.center.showModalCloseable { close -> - CreateProfile(m, close) - if (appPlatform.isDesktop) { - // Hide settings to allow clicks to pass through to CreateProfile view - DisposableEffectOnGone(always = { scope.launch { drawerState.close() } }) { - // Show settings again to allow intercept clicks to close modals after profile creation finishes - scope.launch(NonCancellable) { drawerState.open() } } + withAuth { + ModalManager.center.showModalCloseable { close -> + CreateProfile(m, close) } } }, @@ -71,68 +66,78 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: } }, removeUser = { user -> - 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)) { - append(user.displayName) + 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)) { + append(user.displayName) + } + append(":") } - append(":") - } - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.users_delete_question), - text = text, - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - removeUser(m, user, users, true, searchTextOrPassword.value.trim()) - }) { - Text(stringResource(MR.strings.users_delete_with_connections), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) - } - SectionItemView({ - AlertManager.shared.hideAlert() - removeUser(m, user, users, false, searchTextOrPassword.value.trim()) - } - ) { - Text(stringResource(MR.strings.users_delete_data_only), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.users_delete_question), + text = text, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeUser(m, user, users, true, searchTextOrPassword.value.trim()) + }) { + Text(stringResource(MR.strings.users_delete_with_connections), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeUser(m, user, users, false, searchTextOrPassword.value.trim()) + } + ) { + Text(stringResource(MR.strings.users_delete_data_only), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } } } - } - ) + ) + } }, 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() } } } @@ -145,7 +150,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: @Composable private fun UserProfilesLayout( users: List, - filteredUsers: List, + filteredUsers: List, searchTextOrPassword: MutableState, profileHidden: MutableState, visibleUsersCount: Int, @@ -158,10 +163,7 @@ private fun UserProfilesLayout( unmuteUser: (User) -> Unit, showHiddenProfile: (User) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { if (profileHidden.value) { SectionView { SettingsActionItem(painterResource(MR.images.ic_lock_open_right), stringResource(MR.strings.enter_password_to_show), click = { @@ -205,7 +207,7 @@ private fun UserProfilesLayout( @Composable private fun UserView( - user: User, + userInfo: UserInfo, visibleUsersCount: Int, activateUser: (User) -> Unit, removeUser: (User) -> Unit, @@ -215,7 +217,8 @@ private fun UserView( showHiddenProfile: (User) -> Unit, ) { val showMenu = remember { mutableStateOf(false) } - UserProfilePickerItem(user, onLongClick = { showMenu.value = true }) { + val user = userInfo.user + UserProfilePickerItem(user, onLongClick = { showMenu.value = true }, unreadCount = userInfo.unreadCount) { activateUser(user) } Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { @@ -259,17 +262,14 @@ enum class UserProfileAction { @Composable private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (String) -> Unit) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + 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() @@ -277,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) { @@ -303,21 +303,21 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: ( } } -private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List { +fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List { 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 } @@ -347,6 +347,7 @@ private suspend fun doRemoveUser(m: ChatModel, user: User, users: List, de try { 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) @@ -366,7 +367,9 @@ private suspend fun doRemoveUser(m: ChatModel, user: User, users: List, de m.controller.apiDeleteUser(user, delSMPQueues, viewPwd) } } + 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 + val networkTCPKeepIntvl: MutableState + val networkTCPKeepCnt: MutableState + 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, + smpProxyMode: MutableState, + smpProxyFallback: MutableState, + smpWebPortServers: MutableState, + networkTCPConnectTimeout: MutableState, + networkTCPTimeout: MutableState, + networkTCPTimeoutPerKb: MutableState, + networkRcvConcurrency: MutableState, + networkSMPPingInterval: MutableState, + networkSMPPingCount: MutableState, + networkEnableKeepAlive: MutableState, + networkTCPKeepIdle: MutableState, + networkTCPKeepIntvl: MutableState, + networkTCPKeepCnt: MutableState, + 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, + 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, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSMPProxyFallback: (SMPProxyFallback) -> Unit, + enabled: State, +) { + 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 +) { + 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, values: List, 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, values: List, 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 = remember { mutableStateOf(netCfg.useSocksProxy) } + val currUserServers = remember { stateGetOrPut("currUserServers") { emptyList() } } + val userServers = remember { stateGetOrPut("userServers") { emptyList() } } + val serverErrors = remember { stateGetOrPut("serverErrors") { emptyList() } } + + 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, + onionHosts: MutableState, + currUserServers: MutableState>, + serverErrors: MutableState>, + userServers: MutableState>, + 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, + onionHosts: MutableState, + sessionMode: MutableState, + networkProxy: SharedPreference, + 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, + 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, + onionHosts: MutableState, + 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, + enabled: State, + 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, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSessionMode: (TransportSessionMode) -> Unit, +) { + val density = LocalDensity.current + val values = remember { + val safeModes = TransportSessionMode.safeValues + val modes: Array = + 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>, + userServers: MutableState>, + serverErrors: MutableState>, + 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>, + userServers: MutableState>, + close: () -> Unit, + rhId: Long? +) { + suspend fun acceptForOperators(rhId: Long?, operatorIds: List, 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, 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>, updatedOperators: List) { + 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, + serverErrors: MutableState> +) { + try { + val errors = chatController.validateServers(rhId, userServersToValidate) ?: return + serverErrors.value = errors + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } +} + +fun serversCanBeSaved( + currUserServers: List, + userServers: List, + serverErrors: List +): Boolean { + return userServers != currUserServers && serverErrors.isEmpty() +} + +fun globalServersError(serverErrors: List): String? { + for (err in serverErrors) { + if (err.globalError != null) { + return err.globalError + } + } + return null +} + +fun globalSMPServersError(serverErrors: List): String? { + for (err in serverErrors) { + if (err.globalSMPError != null) { + return err.globalSMPError + } + } + return null +} + +fun globalXFTPServersError(serverErrors: List): String? { + for (err in serverErrors) { + if (err.globalXFTPError != null) { + return err.globalXFTPError + } + } + return null +} + +fun findDuplicateHosts(serverErrors: List): Set { + 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>, + userServers: MutableState> +) { + 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>, + serverErrors: MutableState>, + 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, + 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 +): Pair? { + 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>, + serverErrors: MutableState>, + 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>, + userServers: MutableState>, + serverErrors: MutableState>, + 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>, + serverErrors: MutableState>, + 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>, + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit, + currentUser: User?, + rhId: Long?, + testing: MutableState +) { + 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>, + userServers: MutableState>, + serverErrors: MutableState>, + 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>, + userServers: MutableState>, + serverErrors: MutableState>, + 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, 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?>(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, 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>, 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>, + serverErrors: MutableState>, + 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, + 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, + 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, + 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() } + 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, + 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 = + 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>, + serverErrors: MutableState>, + 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>, + serverErrors: MutableState>, + operatorIndex: Int, + navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit, + currentUser: User?, + rhId: Long?, + testing: MutableState +) { + 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, + xftpServers: List, + testing: MutableState, + onUpdate: (ServerProtocol, List) -> 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>, + serverErrors: MutableState>, + 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) { + 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, + smpServers: List, + xftpServers: List, + m: ChatModel, + onUpdate: (ServerProtocol, List) -> 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): List { + 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, m: ChatModel, onUpdated: (List) -> Unit): Map { + val fs: MutableMap = mutableMapOf() + val updatedServers = ArrayList(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>, + 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>, + 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/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt similarity index 57% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt index 502b579d64..d280773976 100644 --- 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/networkAndServers/ScanProtocolServer.kt @@ -1,37 +1,32 @@ -package chat.simplex.common.views.usersettings +package chat.simplex.common.views.usersettings.networkAndServers -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.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: (ServerCfg) -> Unit) +expect fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) @Composable -fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) { - Column( - Modifier - .fillMaxSize() - ) { +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(ServerCfg(remoteHostId = rhId, text, false, null, true)) + 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 52dd8e6479..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 @@ عن SimpleX أعلاه، ثم: اقبل - لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي. + لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف تعريفك وجهات اتصالك ورسائلك وملفاتك بشكل نهائي. هذه المجموعة لم تعد موجودة. رمز QR هذا ليس رابطًا! - الجيل القادم من الرسائل الخاصة + مستقبل المُراسلة لا يمكن التراجع عن هذا الإجراء - سيتم حذف جميع الملفات والوسائط المستلمة والمرسلة. ستبقى الصور منخفضة الدقة. لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق. ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك. يتم مشاركة ملف التعريف مع جهات اتصالك فقط. - سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة. - سيتم تغيير الدور إلى \"%s\". سيستلم العضو دعوة جديدة. + سيتم تغيير الدور إلى "%s". سيتم إبلاغ كل فرد في المجموعة. + سيتم تغيير الدور إلى "%s". سيستلم العضو دعوة جديدة. خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت. هذا الرابط ليس رابط اتصال صالح! يسمح - أضِف خوادم محدّدة مسبقًا + أضِف خوادم مُعدة مسبقًا أضِف إلى جهاز آخر سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا! - الوصول إلى الخوادم عبر بروكسي SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار. - أضِف خادم… + الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تفعيل هذا الخيار. + أضِف خادم إعدادات الشبكة المتقدمة سيبقى جميع أعضاء المجموعة على اتصال. السماح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك. السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة) - المشرف + المُدير أضِف ملف التعريف السماح بإرسال رسائل مباشرة إلى الأعضاء. قبول التخفي أضِف رسالة ترحيب أضف الخوادم عن طريق مسح رموز QR. - يمكّن للمشرفين إنشاء روابط للانضمام إلى المجموعات. + يمكن للمُدراء إنشاء روابط للانضمام إلى المجموعات. قبول طلب الاتصال؟ سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك. مكالمة مقبولة السماح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك. اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك. يتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن - فهو يسمح لخدمة الإشعارات بالعمل. - يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم ، ويفتح التطبيق كالمعتاد. + يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم، ويفتح التطبيق كالمعتاد. أجب الاتصال - دائِماً + دائمًا السماح بإرسال رسائل تختفي. السماح بإرسال رسائل صوتية. تطبيق ثانوي إضافي - السماح لجهات اتصالك بإضافة ردود الفعل الرسالة. + السماح لجهات اتصالك بإضافة ردود الفعل للرسالة. السماح لجهات اتصالك بالاتصال بك. السماح بردود الفعل على الرسائل. يتم مسح جميع البيانات عند إدخالها. @@ -64,20 +64,20 @@ السماح لجهات اتصالك بإرسال رسائل تختفي. اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك. رمز مرور التطبيق - دائِماً مُتاح + يعمل دائمًا يمكن للتطبيق استلام الإشعارات فقط عند تشغيله، ولن يتم بدء تشغيل أي خدمة في الخلفية السماح بالرسائل الصوتية؟ - ستبقى جميع جهات الاتصال الخاصة بك متصلة. - استخدم التتابع دائمًا + ستبقى جميع جهات اتصالك متصلة. + استخدم الموجه دائمًا النسخ الاحتياطي لبيانات التطبيق حُذفت جميع بيانات التطبيق. السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) اسمح لجهات اتصالك بإرسال رسائل صوتية. - حول عنوان SimpleX + عن عنوان SimpleX بناء التطبيق: %s المظهر - أضف عنوانًا إلى ملف التعريف الخاص بك ، حتى تتمكن جهات الاتصال الخاصة بك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك. - ستبقى جميع جهات الاتصال الخاصة بك متصلة. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك. + أضف عنوانًا إلى ملف تعريفك، حتى تتمكن جهات اتصالك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك. + ستبقى جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك. رمز التطبيق عنوان اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) @@ -96,12 +96,12 @@ يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل. يمكنك أنت وجهة اتصالك إرسال رسائل تختفي. مكالمتك تحت الإجراء - لا يمكّن استلام الملف - جيد للبطارية
. خدمة الخلفية تتحقق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]>
+ لا يمكن استلام الملف + جيد للبطارية. يتحقق التطبيق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]> عريض مكالمات الصوت (ليست مُعمّاة بين الطرفين) الأفضل للبطارية . ستستلم إشعارات فقط عندما يكون التطبيق قيد التشغيل (لا توجد خدمة في الخلفية).]]> - تستهلك المزيد من البطارية ! تعمل خدمة الخلفية دائمًا - تظهر الإشعارات بمجرد توفر الرسائل.]]> + يستهلك المزيد من البطارية! يعمل التطبيق دائمًا في الخلفية - يتم عرض الإشعارات على الفور.]]> انتهت المكالمة بالفعل! تجزئة رسالة سيئة معرّف رسالة سيئ @@ -121,10 +121,9 @@ مصادقة 1 دقيقة 30 ثانية - إلغاء الرسالة المباشرة + ألغِ الرسالة الحيّة إلغاء - سيتم استخدام اتصال TCP منفصل (وبيانات اعتماد SOCKS) لكل جهة اتصال وعضو في المجموعة. -\n الرجاء ملاحظة: إذا كان لديك العديد من التوصيلات ، فقد يكون استهلاك البطارية وحركة المرور أعلى بكثير وقد تفشل بعض الاتصالات. + لكل جهة اتصال وعضو في المجموعة\n. الرجاء ملاحظة: إذا كان لديك العديد من الاتصالات، فقد يكون استهلاك البطارية وحركة المرور أعلى بكثير وقد تفشل بعض الاتصالات.]]> جارٍ الاتصال… مكالمة صوتية المكالمات على شاشة القفل: @@ -136,11 +135,11 @@ يتم استبدال رمز مرور التطبيق برمز مرور التدمير الذاتي. مكالمات الصوت والفيديو خطأ في الاتصال - تحسين البطارية نشط ، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تمكينها عبر الإعدادات. + تحسين البطارية نشط، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تفعيلها عبر الإعدادات. لا يمكن تهيئة قاعدة البيانات إرفاق طلب لاستلام الصورة - نسخة التطبيق: v%s + إصدار التطبيق: v%s قبول تلقائي المكالمات لا يمكن دعوة جهات الاتصال! @@ -175,7 +174,7 @@ تغيير عنوان الاستلام؟ نٌسخت إلى الحافظة مسح - مسح الدردشة + امسح الدردشة إنشاء عنوان الدردشات تأكيد عبارة المرور الجديدة… @@ -183,7 +182,7 @@ قاعدة البيانات مُعمّاة غيرت دور %s إلى %s تغيير عنوان الاستلام - خطأ في إنشاء الملف الشخصي! + خطأ في إنشاء ملف التعريف! خطأ في الإتصال انتهت مهلة الاتصال جهة الاتصال موجودة بالفعل @@ -197,8 +196,8 @@ إنشاء مجموعة سرية خطأ في إحباط تغيير العنوان تفعيل قفل SimpleX - تأكد من بيانات الاعتماد الخاصة بك - إنشاء عنوان SimpleX + تأكد من بيانات اعتمادك + أنشئ عنوان SimpleX متابعة تحدث مع المطورين سياق الأيقونة @@ -241,7 +240,6 @@ تغيير وضع التدمير الذاتي تغيير رمز المرور التدمير الذاتي تأكيد ترقيات قاعدة البيانات - أرشيف الدردشة الاتصال (دعوة مقدمة) مسح خطأ في إنشاء رابط المجموعة @@ -251,8 +249,6 @@ جار الاتصال… أرسلت طلب الاتصال! حُذفت قاعدة بيانات الدردشة - أرشيف الدردشة - نشأ في %1$s جارِ تغيير العنوان… جار الاتصال (قُبِل) فُحصت جهة الاتصال @@ -290,12 +286,12 @@ الاتصال %1$d انشأت الاتصال مكالمة جارية… - عَمِّ + تشفير أدخل عبارة المرور… المنشئ خطأ في إضافة الأعضاء خطأ في إنشاء العنوان - خطأ في حذف اتصال جهة الاتصال المعلق + خطأ في حذف اتصال جهة الاتصال المنتظر أدخل رسالة ترحيب… متصل جار الاتصال @@ -307,18 +303,18 @@ أدخل عبارة المرور الدردشات متصل - سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا الإجراء! + سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا! الحد الأقصى لحجم الملف المدعوم حاليًا هو %1$s. تواصل عبر الرابط / رمز QR إنشاء رابط دعوة لمرة واحدة تحقق من عنوان الخادم وحاول مرة أخرى. - مسح التَحَقُّق + امسح التحقُّق أنشئ عنوانًا للسماح للأشخاص بالتواصل معك. أدخل الخادم يدويًا ملون لدى جهة الاتصال التعمية بين الطريفين إنشاء - إنشاء حسابك الشخصي + أنشئ ملف تعريفك مكالمة جارية... تفعيل التدمير الذاتي الموافقة على التعمية… @@ -334,7 +330,7 @@ خطأ في تغيير الدور %1$d فشل فك تعمية الرسائل. سمة داكنة - حُذِفت + حُذفت عبارة مرور قاعدة البيانات وتصديرها حذف جميع الملفات حذف بعد @@ -348,13 +344,12 @@ تختلف عبارة مرور قاعدة البيانات عن تلك المحفوظة في Keystore. خطأ في قاعدة البيانات ترقية قاعدة البيانات - حذف أرشيف الدردشة؟ حُددت %d جهة اتصال حذف المجموعة حذف المجموعة؟ حذف الرابط - حُذِفت في: %s - المجموعة المحذوفة + حُذفت في: %s + المجموعة حُذفت حذف الصورة تخصيص السمات حذف قاعدة البيانات @@ -371,7 +366,6 @@ أيام حذف العنوان سيتم تحديث عبارة مرور تعمية قاعدة البيانات. - حذف الأرشيف حذف الرابط؟ الرجوع إلى إصدار سابق من قاعدة البيانات قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية. يُرجى تغييره قبل التصدير. @@ -398,7 +392,7 @@ عبارة المرور الحالية… سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في Keystore. معرّف قاعدة البيانات - حُذِفت في + حُذفت في %d يوم %d أيام مخصص @@ -412,14 +406,14 @@ حذف الخادم خطأ في تحديث رابط المجموعة الوصف - توسيع اختيار الدور + توسيع تحديد الدور انتهت صلاحية دعوة المجموعة المجموعة غير موجودة! - تصدير السمة + صدّر السمة الملفات والوسائط قلب الكاميرا سيتم حذف المجموعة لجميع الأعضاء - لا يمكن التراجع عن هذا! - يمكن لأعضاء المجموعة إرسال رسائل مباشرة. + يمكن للأعضاء إرسال رسائل مباشرة. فشل تحميل الدردشات أهلاً! \nتواصل معي عبر SimpleX Chat: %s @@ -428,8 +422,8 @@ الملف حُدّث ملف تعريف المجموعة أدخل اسم المجموعة: - يمكن لأعضاء المجموعة إرسال رسائل صوتية. - الملفات والوسائط ممنوعة في هذه المجموعة. + يمكن للأعضاء إرسال رسائل صوتية. + الملفات والوسائط ممنوعة. رسالة ترحيب المجموعة مزيد من تقليل استخدام البطارية المجموعة @@ -439,16 +433,16 @@ الواجهة الفرنسية المساعدة حُذِفت المجموعة - يمكن لأعضاء المجموعة إرسال رسائل تختفي. + يمكن للأعضاء إرسال رسائل تختفي. إشراف المجموعة أخيرا، لدينا منهم! 🚀 - تصدير قاعدة البيانات + صدّر قاعدة البيانات لوحدة التحكم الميزات التجريبية تجريبي المجموعة غير نشطة الملفات والوسائط - يمكن لأعضاء المجموعة حذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) + يمكن للأعضاء حذف الرسائل المُرسلة بشكل لا رجعة فيه. (24 ساعة) الإصلاح غير مدعوم من قبل جهة الاتصال يُخزّن ملف تعريف المجموعة على أجهزة الأعضاء، وليس على الخوادم. روابط المجموعة @@ -456,32 +450,32 @@ الاسم الكامل: لم تعد دعوة المجموعة صالحة، تمت أُزيلت بواسطة المرسل. رابط المجموعة - سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! + سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! الاسم الكامل للمجموعة: رابط كامل ملف سيتم حذف المجموعة لك - لا يمكن التراجع عن هذا! فشل تحميل الدردشة - يمكن لأعضاء المجموعة إضافة ردود فعل الرسالة. + يمكن للأعضاء إضافة ردود الفعل على الرسائل. المفضل مخفي حُفظ الملف سيتم حذف الملف من الخوادم. - سيتم استلام الملف عند اكتمال تحميل جهة الاتصال الخاصة بك. + سيتم استلام الملف عندما يكتمل جهة اتصالك من رفعِها. المساعدة الملف: %s - إصلاح - إصلاح الاتصال + أصلح + أصلح الاتصال إصلاح الاتصال؟ الإصلاح غير مدعوم من قبل أعضاء المجموعة - يمكن لأعضاء المجموعة إرسال الملفات والوسائط. + يمكن للأعضاء إرسال الملفات والوسائط. تفضيلات المجموعة سريع ولا تنتظر حتى يصبح المرسل متصلاً بالإنترنت! إخفاء كيفية الاستخدام كيف يعمل SimpleX التخفي عبر رابط عنوان جهة الاتصال - رمز الحماية غير صحيحة! + رمز الأمان غير صحيحة! الإشعارات الفورية مُعطَّلة إشعارات فورية! إخفاء جهة الاتصال والرسالة @@ -489,7 +483,7 @@ استيراد قاعدة بيانات ساعات السجل - سيتم استلام الصورة عند اكتمال تحميل جهة اتصالك. + سيتم استلام الصورة عندما يكتمل جهة اتصالك من رفعِها. اعرض رمز QR في مكالمة الفيديو، أو شارك الرابط.]]> ثبّت SimpleX Chat لطرفية إذا قمت بالتأكيد، فستتمكن خوادم المراسلة من رؤية عنوان IP الخاص بك ومزود الخدمة الخاص بك - أي الخوادم التي تتصل بها. @@ -513,11 +507,11 @@ إخفاء شاشة التطبيق في التطبيقات الحديثة. تحسن الخصوصية والأمان مسح رمز QR في مكالمة الفيديو، أو يمكن لجهة الاتصال مشاركة رابط الدعوة.]]> - محصن ضد البريد العشوائي وسوء المعاملة + محصن ضد الإزعاج (spam) التخفي عبر رابط لمرة واحدة أرسلت صورة صورة - سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! + سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! حُفظت الصورة في المعرض صورة إذا لم تتمكن من الالتقاء شخصيًا، اعرض رمز QR في مكالمة الفيديو، أو شارك الرابط. @@ -532,7 +526,7 @@ فوري المضيف إخفاء - يُرجى السماح لSimpleX للتشغيل في الخلفية في مربع الحوار التالي. وإلا، سيتم تعطيل الإشعارات.]]> + السماح بذلك في مربع الحوار التالي لتلقي الإشعارات على الفور.]]> ردًا على إشعارات فورية خوادم ICE (واحد لكل سطر) @@ -540,7 +534,7 @@ إخفاء ملف التعريف كيفية استخدام ماركداون إذا أدخلت رمز مرور التدمير الذاتي أثناء فتح التطبيق: - يمكن تغييره لاحقًا عبر الإعدادات. + كيف يؤثر على البطارية انضمام فاتح مدعو للتواصل @@ -560,20 +554,20 @@ دعوة الأصدقاء خطأ في Keychain دعوة للمجموعة - يٌمنع حذف الرسائل بشكل لا رجعة فيه في هذه المجموعة. + يٌمنع حذف الرسائل بشكل لا رجعة فيه. تنسيق الرسالة غير صالح البيانات غير صالحة - بيانات الملف الشخصي المحلية فقط + بيانات ملف التعريف المحلية فقط يٌمنع حذف الرسائل بشكل لا رجعة فيه في هذه الدردشة. دعوة الأعضاء - مغادرة المجموعة + غادِر المجموعة الاسم المحلي: غادر يسمح بوجود العديد من الاتصالات المجهولة دون مشاركة أي بيانات بينهم في ملف تعريف دردشة واحد. مدعو %1$s مدعو عبر رابط المجموعة الدردشة غير صالحة - حي + حيّ رابط اتصال غير صالح الملف كبير! معرفة المزيد @@ -583,7 +577,7 @@ تأكيد الترحيل غير صالح مدعو رابط معاينة الصورة - رسالة مباشرة! + رسالة حيّة! مائل لنتحدث في SimpleX Chat قفل بعد @@ -591,14 +585,14 @@ انتهت صلاحية الدعوة! انضمام إلى المجموعة؟ الانضمام المتخفي - الانضمام إلى المجموعة + جارِ الانضمام إلى المجموعة غادِر مغادرة المجموعة؟ غادر - إيصالات التسليم معطلة! + إيصالات التسليم مُعطَّلة! تعطيل رسائل تختفي - عٌطل مصادقة الجهاز. جاري إيقاف تشغيل قفل SimpleX. + استيثاق الجهاز مُعطَّل. جارِ إيقاف تشغيل قفل SimpleX. %d شهر %d أسابيع %d أسبوع @@ -609,29 +603,29 @@ نزّل الملف تعطيل قفل SimpleX تحرير - اسم الملف الشخصي: + اسم ملف التعريف: البريد الإلكتروني أدخل أسمك: كرر الرسالة يختفي في: %s الرسائل المختفية ممنوعة في هذه الدردشة. مُعمّى بين الطريفين - حُرر + حُرّر الرجوع إلى إصدار سابق وفتح الدردشة رسائل مباشرة - الرسائل المختفية ممنوعة في هذه المجموعة. - تحرير ملف تعريف المجموعة + الرسائل المختفية ممنوعة. + حرّر ملف تعريف المجموعة لا تُظهر مرة أخرى الجهاز %d أسبوع لا يمكن أن يحتوي اسم العرض على مسافة فارغة. مكالمة فيديو مُعمّاة بين الطريفين - الرسائل المباشرة بين الأعضاء ممنوعة في هذه المجموعة. + الرسائل المباشرة بين الأعضاء ممنوعة في هذه المجموعة. %d ساعة %d ساعة %d ساعات %d شهر - تحرير الصورة + حرّر الصورة %d ملف/ات بإجمالي الحجم %s %d ثانية جهات الاتصال @@ -657,7 +651,7 @@ لا تنشئ عنوانًا خطأ في تحديث تضبيط الشبكة خطأ في استلام الملف - خطأ في تبديل الملف الشخصي! + خطأ في تبديل ملف التعريف! حافظ على اتصالاتك تأكد من أن عناوين خادم XFTP بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة. عُلّم محذوف @@ -689,7 +683,7 @@ خطأ في بدء الدردشة خطأ في تصدير قاعدة بيانات الدردشة ستتم إزالة العضو من المجموعة - لا يمكن التراجع عن هذا! - اجعل الملف الشخصي خاصًا! + اجعل ملف التعريف خاصًا! تصفية الدردشات غير المقروءة والمفضلة. البحث عن الدردشات بشكل أسرع تفعيل @@ -701,17 +695,16 @@ خطأ في إرسال الرسالة خطأ في الانضمام إلى المجموعة خطأ في مزامنة الاتصال - تسجيل الدخول باستخدام بيانات الاعتماد الخاصة بك + سجّل الدخول باستخدام بيانات اعتمادك خطأ في حفظ الملف علّم غير مقروءة تأكد من أن عناوين خادم WebRTC ICE بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة. علّم تحقق منه خطأ في حفظ كلمة مرور المستخدم - إذا SimpleX ليس لديه معرّفات مستخدم، كيف يمكنه توصيل الرسائل؟]]> خطأ في حفظ ملف تعريف المجموعة رسالة نصية ردود فعل الرسائل - سيتم وضع علامة على الرسالة للحذف. سيتمكن المستلم/مون من الكشف عن هذه الرسالة. + سيتم وضع علامة على الرسالة للحذف. سيتمكن المُستلم/ون من الكشف عن هذه الرسالة. سيتم حذف الرسالة - لا يمكن التراجع عن هذا! خطأ في تسليم الرسالة الشبكة والخوادم @@ -719,7 +712,7 @@ فتح في تطبيق الجوال، ثم انقر فوق اتصال في التطبيق.]]> تحت الإشراف في: %s ردود الفعل الرسائل ممنوعة في هذه الدردشة. - مُشرف بواسطة %s + أُشرف بواسطة %s من المرجح أن جهة الاتصال هذه قد حذفت الاتصال بك. ردود فعل الرسائل مكالمة فائتة @@ -736,14 +729,12 @@ إيصالات تسليم الرسائل! دقائق شهور - - توصيل رسائل أكثر استقرارًا. -\n- مجموعات أفضل قليلاً. -\n- و اكثر! + - توصيل رسائل أكثر استقرارًا.\n- مجموعات أفضل قليلاً.\n- و اكثر! حالة الشبكة كتم - ردود الفعل الرسائل ممنوعة في هذه المجموعة. + ردود الفعل الرسائل ممنوعة. المزيد - إعدادات الشبكة + إعدادات متقدّمة مكالمة فائتة مسودة الرسالة ملفات تعريف دردشة متعددة @@ -764,16 +755,13 @@ سيتم استخدام مضيفات البصل عند توفرها. لن يتم استخدام مضيفات البصل. لم تٌحدد جهات اتصال - يمكّن للمشرف الآن: -\n- حذف رسائل الأعضاء. -\n- تعطيل الأعضاء (دور \"المراقب\") + يمكن للمُدراء الآن:\n- حذف رسائل الأعضاء.\n- تعطيل الأعضاء (دور المراقب) خدمة الإشعار غير مفعّل` مفعل أرشيف قاعدة البيانات القديمة غير مفعّل رابط دعوة لمرة واحدة - سوف تكون مضيفات البصل مطلوبة للاتصال. المراقب لا يوجد نص دور عضو جديد @@ -787,67 +775,62 @@ يمكن إرسال 10 فيديوهات فقط في نفس الوقت رابط دعوة لمرة واحدة لا - سوف تكون مضيفات البصل مطلوبة للاتصال. -\nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion. - سيتم استخدام مضيفات البصل عند توفرها. - لن يتم استخدام مضيفات البصل. + ستكون مضيفات البصل مطلوبة للاتصال. \nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion. اسم عرض جديد: عبارة مرور جديدة… - يرجى الانتظار - كلمة المرور مطلوبة + قيد الانتظار + عبارة المرور مطلوبة ألصِق الرابط الذي استلمته فقط مالكي المجموعة يمكنهم تفعيل الملفات والوسائط. فقط مالكي المجموعة يمكنهم تفعيل الرسائل الصوتية. (يخزن فقط بواسطة أعضاء المجموعة) - كلمة المرور - تم تعيين كلمة المرور! + رمز المرور + عيّنت رمز المرور! المالك فقط جهة اتصالك يمكنها إرسال رسائل تختفي. - جهة اتصالك فقط يمكنها إضافة تفاعلات على الرسالة + جهة اتصالك فقط يمكنها إضافة ردود الفعل على الرسالة فقط مالكي المجموعة يمكنهم تغيير تفضيلات المجموعة. جهة اتصالك فقط يمكنها حذف الرسائل بشكل لا رجعة فيه (يمكنك تعليم الرسالة للحذف). (24 ساعة) أنت فقط يمكنك إرسال رسائل صوتية. افتح - لم يتم تغيير كلمة المرور! - تم تغيير كلمة المرور + لم يتم تغيير رمز المرور! + تم تغيير رمز المرور جارِ فتح قاعدة البيانات… جهة اتصالك فقط يمكنها إرسال رسائل صوتية. ألصق - كلمة المرور غير موجودة في مخزن المفاتيح، يرجى إدخالها يدوياً. قد يحدث هذا إذا قمت باستعادة ملفات التطبيق باستخدام أداة استرجاع بيانات. إذا لم يكن الأمر كذلك، تواصل مع المبرمجين رجاء + لم يتم العثور على عبارة المرور في Keystore، يُرجى إدخالها يدويًا. ربما حدث هذا إذا استعدت بيانات التطبيق باستخدام أداة النسخ الاحتياطي. إذا لم يكن الأمر كذلك، يُرجى التواصل مع المطورين. افتح الدردشة - فتح الرابط في المتصفح قد يقلل خصوصية وحماية اتصالك. الروابط غير الموثوقة من SimpleX ستكون باللون الأحمر - أنت فقط يمكنك إضافة تفاعل على الرسالة. + قد يؤدي فتح الرابط في المتصفح إلى تقليل خصوصية الاتصال وأمانه. ستظهر روابط SimpleX غير الموثوقة باللون الأحمر. + أنت فقط يمكنك إضافة ردود الفعل على الرسالة. أنت فقط يمكنك حذف الرسائل بشكل لا رجعة فيه (يمكن للمستلم تعليمها للحذف). (24 ساعة) أنت فقط يمكنك إرسال رسائل تختفي أنت فقط يمكنك إجراء المكالمات. فقط جهة اتصالك يمكنها إجراء المكالمات. افتح وحدة تحكم الدردشة - إدخال كلمة المرور - افتح SimpleX Chat للرد على المكالمة - بروتوكول وكود مفتوح المصدر - يمكن لأي شخص تشغيل الخوادم. + إدخال رمز المرور + افتح SimpleX Chat لقبول المكالمة + يمكن لأي شخص استضافة الخوادم. كلمة المرور للإظهار ندّ لِندّ - يمكن للناس التواصل معك فقط عبر الرابط الذي تقوم بمشاركته - مكالمة في الانتظار - تعمية ثنائية الطبقات من بين الطريفين.]]> - إعادة تعيين الألوان + أنت تقرر مَن يمكنه الاتصال. + مكالمة قيد الانتظار + تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل. + صفّر الألوان حفظ - عنوان الخادم المحدد مسبقًا + عنوان الخادم المُعد مسبقًا حفظ وإشعار أعضاء المجموعة دوري أعد تشغيل التطبيق لاستخدام قاعدة بيانات الدردشة المستوردة. الاستلام عبر يُرجى التحقق من استخدامك للرابط الصحيح أو اطلب من جهة اتصالك أن ترسل لك رابطًا آخر. الإشعارات الدورية مُعطَّلة - صورة الملف الشخصي + صورة ملف التعريف الإشعارات خاصة يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. يُرجى تحديث التطبيق والتواصل مع المطورين. دليل المستخدم.]]> - افتح ملفات تعريف الدردشة + غيّر ملفات تعريف الدردشة اسحب الوصول - حفظ الأرشيف - حفظ اللون كشف سيتم إيقاف استلام الملف. رفض @@ -874,7 +857,7 @@ الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء. استعادة النسخة الاحتياطية لقاعدة البيانات؟ حفظ - اتصالات الملف الشخصي والخادم + اتصالات ملف التعريف والخادم منع ردود فعل الرسالة. منع إرسال الرسائل الصوتية. منع ردود فعل الرسائل. @@ -893,11 +876,10 @@ حٌديثت السجل في حٌديثت السجل في: %s استعادة - إرجاع - يرى المستلمون التحديثات أثناء كتابتها. + يرى المُستلمون التحديثات أثناء كتابتها. استلمت، ممنوع حفظ - سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك. + سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك. حفظ وإشعار جهات الاتصال حفظ وتحديث ملف تعريف المجموعة عدد البينج @@ -911,9 +893,9 @@ استلمت في إزالة العضو إزالة - إعادة التعيين إلى الإعدادات الافتراضية + صفّر إلى الإعدادات الافتراضية بينج الفاصل الزمني - كلمة مرور الملف الشخصي + كلمة مرور ملف التعريف منع إرسال الرسائل التي تختفي. مهلة البروتوكول مهلة البروتوكول لكل كيلوبايت @@ -922,7 +904,7 @@ منع إرسال الرسائل التي تختفي. الاحتفاظ بمسودة الرسالة الأخيرة، مع المرفقات. أسماء ملفات خاصة - حماية ملفات تعريف الدردشة الخاصة بك بكلمة مرور! + احمِ ملفات تعريف دردشتك بكلمة مرور! رُفضت المكالمة حماية شاشة التطبيق أُزيلت @@ -930,19 +912,18 @@ معاينة من المحتمل أن الملف المرجعي للشهادة في عنوان الخادم غير صحيح يتم استلام الرسائل… - يُرجى الاتصال بمشرف المجموعة. + يُرجى الاتصال بمُدير المجموعة. أعد التفاوض إعادة تفاوض التعمية سحب وصول الملف سحب وصول الملف؟ رٌفض الإذن! يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية. - العنصر النائب لصورة الملف الشخصي + العنصر النائب لصورة ملف التعريف رمز QR - إعادة التعيين + صفّر المنفذ %d - خادم محدد مسبقًا - قراءة المزيد في مستودعنا على GitHub. + خادم مُعد مسبقًا يتم استخدام خادم الترحيل فقط إذا لزم الأمر. يمكن لطرف آخر مراقبة عنوان IP الخاص بك. حفظ وإشعار جهة الاتصال إعادة التشغيل @@ -951,12 +932,12 @@ %s في %s حفظ ملف المجموعة ثانوي - كلمة مرور التدمير الذاتي + رمز المرور للتدمير الذاتي إرسال الملفات غير مدعوم بعد - قام المرسل بإلغاء إرسال الملف + أُلغيَ المرسل نقل الملف. (امسح أو ألصق من الحافظة) ثانية - قد يكون المرسل قد ألغى طلب الاتصال + ربما حذف المرسل طلب الاتصال. مسح رمز QR أرسل لنا بريداً مسح رمز الأمان من تطبيق جهة الاتصال @@ -964,65 +945,65 @@ سيتم إيقاف إرسال الملف. إرسال رسالة إرسال - إرسال رسالة حية + أرسل رسالة حيّة فشلت تجربة الخادم! - حفظ كلمة المرور في مخزن المفاتيح + احفظ عبارة المرور في Keystore أرسل رسالة مباشرة إرسال عبر الخوادم تقييم الأمان الرسائل المرسلة سيتم حذفها بعد المدة المحدّدة. تعيين رسالة تظهر للأعضاء الجدد! - تعيين كلمة المرور + عيّن رمز المرور تم إرساله في: %s %s (الحالي) رسالة مرسلة عيّن تفضيلات المجموعة - عيينها بدلا من توثيق النظام + عيّنها بدلاً من استيثاق النظام. مشاركة إرسال - حفظ كلمة المرور وفتح الدردشة - اختيار جهات اتصال + احفظ عبارة المرور وافتح الدردشة + حدد جهات الاتصال تعيين يوم واحد ثواني رسالة مرسلة أرسل رسالة تختفي - حفظ كلمة مرور الحساب + حفظ كلمة مرور ملف التعريف تدمير ذاتي - مسح الكود - إرسال أسئلة وأفكار + مسح الرمز + أرسل أسئلة وأفكار مشاركة العنوان مع جهات الاتصال؟ مشاركة العنوان حفظ رسالة الترحيب؟ - حفظ السيرفرات - أرسل تقارير الاستلام إلى - إرسال تقارير الاستلام معطل لـ %d جهة اتصال. - إرسال تقارير الاستلام مفعل لـ %d جهة اتصال - تعيين كلمة المرور للتصدير + احفظ الخوادم + أرسل إيصالات التسليم إلى + إرسال الإيصالات مُعطَّل لـ %d جهة اتصال. + إرسال الإيصالات مفعّل لـ %d جهة اتصال + عيّن عبارة المرور للتصدير تم تغيير رمز الأمان - تقارير الارسال + إيصالات الإرسال تم إرساله في - اختيار - إرسال تقارير الاستلام سيتم تفعيله لجميع جهات الاتصال. - سيتم تفعيل إرسال تقارير الاستلام لجميع جهات الاتصال ذات حسابات دردشة ظاهرة + حدد + سيتم تفعيل إرسال إيصالات التسليم لجميع جهات الاتصال. + سيتم تفعيل إرسال إيصالات التسليم لجميع جهات الاتصال في جميع ملفات تعريف الدردشة المرئية. قائمة انتظار آمنة فشل الإرسال - تم الإرسال + أُرسلت تعيين اسم جهة الاتصال… - إرسال رسالة حية - سيتم تحديثها للمستلم مع كتابتك لها + أرسل رسالة حيّة - سيتم تحديثها للمُستلم مع كتابتك لها تعيين اسم جهة الاتصال الإعدادات حفظ الخوادم؟ مسح رمز QR الخادم رمز الأمان - حفظ الإعدادات؟ + حفظ التفضيلات؟ حفظ الإعدادات؟ سري - كلمة مرور التدمير الذاتي - تم تغيير كلمة مرور التدمير الذاتي! - تم تفعيل كلمة مرور التدمير الذاتي + رمز المرور للتدمير الذاتي + تغيّرت رمز المرور للتدمير الذاتي! + فعّلت رمز المرور للتدمير الذاتي! الإعدادات - دعوة لمرة واحدة SimpleX + دعوة SimpleX لمرة واحدة عرض جهة الاتصال والرسالة قفل SimpleX لم يتحقق من %s @@ -1030,7 +1011,7 @@ خوادم SMP مشاركة الوسائط… رسائل SimpleX Chat - لم يتم تمكين قفل SimpleX! + قفل SimpleX غير مفعّل! إيقاف الدردشة التوقف عن استلام الملف؟ مشاركة الملف… @@ -1043,13 +1024,13 @@ تخطي دعوة الأعضاء إيقاف الدردشة؟ عرض - حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد - قد ترى وحدة تحكم الدردشة لمزيد من التفاصيل. + حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد: وكيل SOCKS تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits. إيقاف عرض المعاينة السماعة متوقفة - وضع قفل SimpleX + SimpleX وضع القفل مشاركة الرابط الرسائل التي تم تخطيها عنوان SimpleX @@ -1063,7 +1044,6 @@ إيقاف مشاركة العنوان؟ إيقاف المشاركة أوقف الدردشة لتصدير أو استيراد أو حذف قاعدة بيانات الدردشة. لن تتمكّن من استلام الرسائل وإرسالها أثناء إيقاف الدردشة. - أوقف الدردشة لتمكين إجراءات قاعدة البيانات. %s ثانية/ثواني يبدأ… تم تشغيل القفل SimpleX @@ -1071,7 +1051,7 @@ عرض خيارات المطور simplexmq: v%s (%2s) يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور - يتطلب الخادم إذنًا للتحميل، تحقق من كلمة المرور + يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور عرض جهة الاتصال فقط مكالمات SimpleX Chat خدمة SimpleX Chat @@ -1080,13 +1060,13 @@ إيقاف الملف التوقف عن إرسال الملف؟ عنوان SimpleX - استخدم مضيفي .onion إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.]]> + استخدم مضيفي .onion إلى "لا" إذا كان وكيل SOCKS لا يدعمها.]]> مشاركة مع جهات الاتصال إيقاف التشغيل؟ إعدادات وكيل SOCKS إيقاف التشغيل السماعة قيد التشغيل - إرسال + أرسل النظام السمة لبدء محادثة جديدة @@ -1094,46 +1074,45 @@ لامركزية بالكامل – مرئية للأعضاء فقط. النظام فشل الاختبار في الخطوة %s. - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! يجلب التطبيق الرسائل الجديدة بشكل دوري - يستخدم نسبة قليلة من البطارية يوميًا. لا يستخدم التطبيق إشعارات الدفع - لا يتم إرسال البيانات من جهازك إلى الخوادم. سيتم إلغاء الاتصال الذي قبلته! لن تتمكن جهة الاتصال التي شاركت هذا الرابط معها من الاتصال! هذا النص متاح في الإعدادات - لحماية الخصوصية، بدلاً من معرفات المستخدم التي تستخدمها جميع الأنظمة الأساسية الأخرى, يحتوي SimpleX على معرفات لقوائم انتظار الرسائل، منفصلة لكل جهة من جهات اتصالك. - لحماية معلوماتك، قم بتشغيل قفل SimpleX -\nسيُطلب منك إكمال المصادقة قبل تمكين هذه الميزة. + لحماية خصوصيتك، يستخدم SimpleX معرّفات منفصلة لكل جهة اتصال لديك. + لحماية معلوماتك، فعّل قفل SimpleX \nسيُطلب منك إكمال المصادقة قبل تفعيل هذه الميزة. عزل النقل - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! دعم البلوتوث وتحسينات أخرى. - بفضل المستخدمين - المساهمة عبر Weblate! - خدمة SimpleX تعمل في الخلفية – يستخدم نسبة قليلة من البطارية يوميًا.]]> + بفضل المستخدمين - ساهِم عبر Weblate! + يتم تشغيل SimpleX في الخلفية بدلاً من استخدام إشعارات push.]]> انقر لبدء محادثة جديدة (للمشاركة مع جهة اتصالك) للتواصل عبر الرابط للاتصال، يمكن لجهة الاتصال مسح رمز QR أو استخدام الرابط في التطبيق. - خوادم الاختبار - المنصة الأولى بدون أي معرفات للمستخدم - صمّمناه ليكون خاصًا. + اختبر الخوادم + لا معرّفات مُستخدم دعم SIMPLEX CHAT تبديل العنوان الرئيسي سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء. انقر للانضمام - للكشف عن ملف التعريف المخفي الخاص بك، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك. + للكشف عن ملف تعريفك المخفي، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف دردشتك. انقر للانضمام إلى وضع التخفي النظام السمات - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! قاعدة البيانات لا تعمل بشكل صحيح. انقر لمعرفة المزيد - ألوان السمة - انقر لتنشيط الملف الشخصي. + ألوان الواجهة + انقر لتنشيط ملف التعريف. عزل النقل هذه السلسلة ليست رابط اتصال! - هذه الإعدادات لملف التعريف الحالي الخاص بك + هذه الإعدادات لملف تعريفك الحالي يمكن تجاوزها في إعدادات الاتصال و المجموعة. انتهت مهلة اتصال TCP لحماية المنطقة الزمنية، تستخدم ملفات الصور / الصوت التوقيت العالمي المنسق (UTC). فقدنا القراد الثاني! ✅ - بفضل المستخدمين - المساهمة عبر Weblate! + بفضل المستخدمين - ساهِم عبر Weblate! لم تكتمل محاولة تغيير عبارة مرور قاعدة البيانات. لاستلام الإشعارات، يُرجى إدخال عبارة مرور قاعدة البيانات مصادقة النظام @@ -1144,7 +1123,7 @@ مقاطع الفيديو كثيرة! زر النقر شكرًا لك على تثبيت SimpleX Chat! - خادم الاختبار + اختبر الخادم تجزئة الرسالة السابقة مختلفة. معرف الرسالة التالية غير صحيح (أقل أو يساوي السابق). \nيمكن أن يحدث ذلك بسبب بعض العلل أو عندما يُخترق الاتصال. @@ -1155,39 +1134,38 @@ محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s). تشغيل خوادم WebRTC ICE - أنت تستخدم ملفًا شخصيًا متخفيًا لهذه المجموعة - لمنع مشاركة ملفك الشخصي الرئيسي الذي يدعو جهات الاتصال غير مسموح به + أنت تستخدم ملف تعريف متخفي لهذه المجموعة - لمنع مشاركة ملفك التعريفي الرئيسي الذي يدعو جهات الاتصال غير مسموح به غيّرتَ دور %s إلى %s نعم أنت متصل بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه. أنت لقد شاركت رابط لمرة واحدة سيتم إرسال ملف التعريفك إلى جهة الاتصال التي استلمت منها هذا الرابط. - سوف تتصل بجميع أعضاء المجموعة. - ملفات تعريف الدردشة الخاصة بك + ستتصل بجميع أعضاء المجموعة. + ملفات تعريف دردشتك عنوان SimpleX الخاص بك خوادم SMP الخاصة بك - هل تريد تحديث إعداد مضيفي onion.؟ عندما يكون التطبيق قيد التشغيل عبر المُرحل لقد انضممت إلى هذه المجموعة لقد رفضت دعوة المجموعة - عندما تشارك ملفًا شخصيًا متخفيًا مع شخص ما، فسيتم استخدام هذا الملف الشخصي للمجموعات التي يدعوك إليها. + عندما تشارك ملف تعريف متخفي مع شخص ما، فسيتم استخدام هذا الملف التعريفي للمجموعات التي يدعوك إليها. لديك بالفعل ملف تعريف دردشة بنفس اسم العرض. الرجاء اختيار اسم آخر. أنت متصل بالفعل بـ%1$s. في انتظار الفيديو - سيتم استلام الفيديو عند اكتمال تحميل جهة اتصالك. - تحقق من رمز الحماية + سيتم استلام الفيديو عند اكتمال رفع جهة اتصالك. + تحقق من رمز الأمان رسائل صوتية عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه. - سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! - سوف تكون متصلاً عندما يتم قبول طلب الاتصال الخاص بك، يرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا! تستخدم خوادم SimpleX Chat. استخدم وكيل SOCKS استخدم مضيفي onion. استخدام وكيل SOCKS؟ عندما تكون متاحة ستبقى جهات اتصالك متصلة. - لا نقوم بتخزين أي من جهات الاتصال أو الرسائل الخاصة بك (بمجرد تسليمها) على الخوادم. + لا نقوم بتخزين أي من جهات اتصالك أو رسائلك (بمجرد تسليمها) على الخوادم. يمكنك استخدام تخفيض السعر لتنسيق الرسائل: استخدم الدردشة أنت @@ -1222,22 +1200,21 @@ عبر رابط لمرة واحدة مكالمة الفيديو ليست مُعمّاة بين الطريفين غيّرتَ العنوان - سوف تكون متصلاً عندما يكون جهاز جهة الاتصال الخاصة بك متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! + ستكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! غادرت - يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال. - سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! - لاستلام الرسائل وجهات اتصالك - الخوادم التي تستخدمها لمراسلتهم.]]> + يجب عليك استخدام أحدث إصدار من قاعدة بيانات دردشتك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال. + سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s. أُزيلت %1$s - تحديث - قاعدة بيانات الدردشة الخاصة بك غير مُعمّاة - عيّن عبارة مرور لحمايتها. + حدّث + قاعدة بيانات دردشتك غير مُعمّاة - عيّن عبارة مرور لحمايتها. عبارة مرور قاعدة بيانات خاطئة سيتم إرسال ملف تعريف الدردشة الخاص بك إلى أعضاء المجموعة مرحبًا! %1$s يريد الاتصال بك! خوادم ICE الخاصة بك خصوصيتك - حدثت ملف تعريف المجموعة + حُدثت ملف تعريف المجموعة أنت: %1$s تحديث تحديث إعدادات الشبكة؟ @@ -1255,11 +1232,11 @@ سيتم حذف قاعدة بيانات الدردشة الحالية واستبدالها بالقاعدة المستوردة. \nلا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي. تحديث عبارة مرور قاعدة البيانات - سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ سجل الدردشة. + ستتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ بسجل الدردشة. أسابيع يمكنك إخفاء أو كتم ملف تعريف المستخدم - اضغط مطولاً للقائمة. ما هو الجديد - ملفك الشخصي الحالي + ملف تعريفك الحالي عبر %1$s غير مقروءة مرحبًا! @@ -1269,9 +1246,9 @@ فيديو يمكنك مشاركة عنوانك كرابط أو رمز QR - يمكن لأي شخص الاتصال بك. يمكنك إنشاؤه لاحقًا - أنت تحاول دعوة جهة اتصال قمت بمشاركة ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملفك الشخصي الرئيسي - إلغاء الكتم - إلغاء الكتم + أنت تحاول دعوة جهة اتصال شاركت ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملف تعريفك الرئيسي + ألغِ الكتم + ألغِ الكتم لقد قبلت الاتصال إلغاء إخفاء ملف تعريف يجب أن تكون جهة الاتصال متصلة بالإنترنت حتى يكتمل الاتصال. @@ -1280,18 +1257,16 @@ استخدم للاتصالات الجديدة استخدم الخادم عنوان خادمك - قاعدة بيانات الدردشة الخاصة بك + قاعدة بيانات دردشتك أنت مدعو إلى المجموعة. انضم للتواصل مع أعضاء المجموعة. - لقد انضممت إلى هذه المجموعة. الاتصال بدعوة عضو المجموعة. + لقد انضممت إلى هذه المجموعة. جارِ الاتصال بدعوة عضو المجموعة. غيّرتَ العنوان ل%s إلغاء إخفاء ملف تعريف الدردشة الرسائل الصوتية ممنوعة في هذه الدردشة. مقاطع فيديو وملفات تصل إلى 1 جيجا بايت - - رسائل صوتية تصل إلى 5 دقائق. -\n- الوقت المخصص لتختفي. -\n- تحرير التاريخ. + - رسائل صوتية تصل إلى 5 دقائق.\n- الوقت المخصص لتختفي.\n- تحرير التاريخ. يمكنك تفعيلة لاحقًا عبر الإعدادات - يمكنك تمكينها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق. + يمكنك تفعيلها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق. عبر رابط المجموعة لقد شاركت رابط لمرة واحدة متخفي عبر المتصفح @@ -1304,11 +1279,11 @@ سيتم إرسال ملف تعريف الدردشة الخاص بك \nإلى جهة اتصالك إلغاء الإخفاء - ملفك الشخصي العشوائي - ستستمر في استلام المكالمات والإشعارات من الملفات الشخصية المكتومة عندما تكون نشطة. + ملفك التعريفي العشوائي + ستستمر في استلام المكالمات والإشعارات من الملفات التعريفية المكتومة عندما تكون نشطة. انت تسمح بها مكالمة فيديو - الرسائل الصوتية ممنوعة في هذه الدردشة. + الرسائل الصوتية ممنوعة. فتح القفل رفع الملف لا يمكن التحقق منك؛ الرجاء المحاولة مرة اخرى. @@ -1316,7 +1291,7 @@ رسالة صوتية… أنت مدعو إلى المجموعة لا يمكنك إرسال رسائل! - تحتاج إلى السماح لجهة الاتصال الخاصة بك بإرسال رسائل صوتية لتتمكن من إرسالها. + تحتاج إلى السماح لجهة اتصالك بإرسال رسائل صوتية لتتمكن من إرسالها. أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s). الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات.]]> خادمك @@ -1351,7 +1326,7 @@ لا يمكن تشغيل SimpleX في الخلفية. ستستلم الإشعارات فقط عندما يكون التطبيق قيد التشغيل. سيتم مشاركة ملف تعريف عشوائي جديد. ألصق الرابط المُستلَم للتواصل مع جهة اتصالك… - ستتم مشاركة ملفك الشخصي %1$s. + ستتم مشاركة ملفك التعريفي %1$s. قد يغلق التطبيق بعد دقيقة واحدة في الخلفية. سماح لا مكالمات في الخلفية @@ -1370,12 +1345,12 @@ سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في الإعدادات. هل تريد إزالة عبارة المرور من الإعدادات؟ استخدم عبارة مرور عشوائية - حفظ عبارة المرور في الإعدادات - إعداد كلمة المرور لقاعدة البيانات - تعيين عبارة مرور قاعدة البيانات + احفظ عبارة المرور في الإعدادات + إعداد عبارة المرور لقاعدة البيانات + عيّن عبارة مرور قاعدة البيانات افتح مجلد قاعدة البيانات سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق. - يُخزين عبارة المرور في الإعدادات كنص عادي. + عبارة المرور مخزنة في الإعدادات كنص عادي. يُرجى الملاحظة: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]> عَمِّ الملفات المحلية عَمِّ الملفات والوسائط المخزنة @@ -1385,9 +1360,7 @@ اكتشاف والانضمام إلى المجموعات العربية والبلغارية والفنلندية والعبرية والتايلاندية والأوكرانية - شكرًا للمستخدمين و Weblate. إنشاء ملف تعريف جديد في تطبيق سطح المكتب. 💻 - - الاتصال بخدمة الدليل (تجريبي)! -\n- إيصالات التسليم (ما يصل إلى 20 عضوا). -\n- أسرع وأكثر استقرارًا. + - الاتصال بخدمة الدليل (تجريبي)!\n- إيصالات التسليم (ما يصل إلى 20 عضوا).\n- أسرع وأكثر استقرارًا. افتح حدث خطأ أثناء إنشاء جهة اتصال للعضو أرسل رسالة مباشرة للاتصال @@ -1404,14 +1377,14 @@ سيتم إخفاء كافة الرسائل الجديدة من %s! محظور حظر أعضاء المجموعة - جهة الاتصال المحذوفة - أنشِئ مجموعة باستخدام ملف تعريف عشوائي. - أنشِئ مجموعة - أنشِئ ملف تعريف + جهة الاتصال حُذفت + أنشئ مجموعة باستخدام ملف تعريف عشوائي. + أنشئ مجموعة + أنشئ ملف تعريف سطح المكتب متصل اتصل تلقائيًا عنوان سطح المكتب - وضّع علامة \"محذوفة\" على %d من الرسائل + وضّع علامة "محذوفة" على %d من الرسائل اكتشف عبر الشبكة المحلية اتصل عبر الرابط؟ اتصل بنفسك؟ @@ -1464,7 +1437,7 @@ مجموعات التخفي %s و%s و%d عضو هذا الجهاز - %1$d من الرسائل يُشرف عليها بواسطة %2$s + %1$d من الرسائل أُشرف عليها بواسطة %2$s إلغاء حظر العضو %s قُطع اتصاله]]> في انتظار سطح المكتب… @@ -1502,9 +1475,7 @@ تحقق من الرمز مع سطح المكتب مسح رمز QR من سطح المكتب إلغاء الحظر - - إشعار اختياريًا جهات الاتصال المحذوفة. -\n- أسماء الملفات الشخصية بمسافات. -\n- و اكثر! + - إشعار اختياريًا جهات الاتصال المحذوفة.\n- أسماء الملفات التعريفية بمسافات.\n- و اكثر! مسار الملف غير صالح لقد طلبت بالفعل الاتصال عبر هذا العنوان! إظهار وحدة التحكم في نافذة جديدة @@ -1539,7 +1510,7 @@ يمكنك عرض رابط الدعوة مرة أخرى في تفاصيل الاتصال. أبقِ الدعوة غير المستخدمة؟ شارك رابط الدعوة هذا لمرة واحدة - أنشِئ مجموعة: لإنشاء مجموعة جديدة.]]> + أنشئ مجموعة: لإنشاء مجموعة جديدة.]]> التاريخ المرئي رمز مرور التطبيق دردشة جديدة @@ -1567,7 +1538,7 @@ %s غير نشط]]> أظهر مكالمات API البطيئة غير معروف - حدّثت الملف الشخصي + حدّثت ملف التعريف %s مفقود]]> %s لديه إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين]]> %s في حالة سيئة]]> @@ -1592,9 +1563,9 @@ خيارات المطور تغيّر العضو %1$s إلى %2$s أزلت عنوان الاتصال - أزلت الصورة الشخصية + أزلت صورة ملف التعريف عيّن عنوان جهة اتصال جديد - عيّن صورة شخصية جديدة + عيّن صورة تعريفية جديدة حالة غير معروفة تغيّر جهة الاتصال %1$s إلى %2$s يستغرق تنفيذ الوظيفة وقتًا طويلاً جدًا: %1$d ثانية: %2$s @@ -1618,7 +1589,7 @@ إلغاء حظر العضو للجميع؟ إلغاء الحظر للجميع حدث خطأ أثناء حظر العضو للجميع - حُظر %d رسالة من قبل المشرف + حُظرت %d رسالة من قبل المُدير محظور %s أُلغيت حظر %s حظرت %s @@ -1626,8 +1597,8 @@ محظور حظر للجميع حظر العضو للجميع؟ - محظور من قبل المشرف - محظور من قبل المشرف + محظور من قبل المُدير + محظور من قبل المُدير الرسالة كبيرة جدًا رسالة الترحيب طويلة جدًا ترحيل قاعدة البيانات قيد التقدم. @@ -1638,10 +1609,10 @@ متصفح الويب الافتراضي مطلوب للمكالمات. يُرجى تضبيط المتصفح الافتراضي في النظام، ومشاركة المزيد من المعلومات مع المطورين. حدث خطأ أثناء فتح المتصفح أرشفة و رفع - يمكن للمشرفين حظر عضو للجميع. + يمكن للمُدراء حظر عضو للجميع. ترحيل بيانات التطبيق جارِ أرشفة قاعدة البيانات - سيتم تعمية جميع جهات الاتصال والمحادثات والملفات الخاصة بك بشكل آمن وتحميلها في أجزاء إلى مُرحلات XFTP التي ضبطت. + جميع جهات الاتصال، المحادثات والملفات الخاصة بك سيتم تشفيرها بأمان ورفعها على شكل أجزاء إلى موجهات XFTP المُعدة. طبّق يُرجى ملاحظة: استخدام نفس قاعدة البيانات على جهازين سيؤدي إلى كسر فك تعمية الرسائل من اتصالاتك، كحماية أمنية.]]> تحذير: سيتم حذف الأرشيف.]]> @@ -1675,7 +1646,7 @@ ألصق رابط الأرشيف يمكنك إعطاء محاولة أخرى. حدث خطأ أثناء تنزيل الأرشيف - الملف المُصدر غير موجود + الملف المُصدّر غير موجود تحقق من عبارة المرور تأكد من أنك تتذكر عبارة مرور قاعدة البيانات لترحيلها. التحقق من عبارة مرور قاعدة البيانات @@ -1735,9 +1706,9 @@ السماح بإرسال روابط SimpleX. منع إرسال روابط SimpleX كل الأعضاء - يمكن لأعضاء المجموعة إرسال روابط SimpleX. - روابط SimpleX محظورة في هذه المجموعة. - المشرفين + يمكن للأعضاء إرسال روابط SimpleX. + روابط SimpleX محظورة. + المُدراء مفعّل لـ المالكون الملفات والوسائط غير مسموح بها @@ -1763,8 +1734,646 @@ عند اتصال بمكالمات الصوت والفيديو. إدارة الشبكة اتصال شبكة أكثر موثوقية. - صور الملف الشخصي - شكل الصور الشخصية + صور ملف التعريف + شكّل الصور التعريفية واجهة المستخدم الليتوانية مربع أو دائرة أو أي شيء بينهما. - \ No newline at end of file + عنوان الخادم غير متوافق مع إعدادات الشبكة. + إصدار الخادم غير متوافق مع إعدادات الشبكة. + مفتاح خاطئ أو اتصال غير معروف - على الأرجح حُذف هذا الاتصال. + تم تجاوز السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا. + خطأ في خادم الوجهة: %1$s + خطأ: %1$s + خادم التحويل: %1$s +\nخطأ في الخادم الوجهة: %2$s + خادم التحويل: %1$s +\nخطأ: %2$s + تحذير تسليم الرسالة + مشكلات الشبكة - انتهت صلاحية الرسالة بعد عِدة محاولات لإرسالها. + نعم + لحماية عنوان IP الخاص بك، يستخدم التوجيه الخاص خوادم SMP الخاصة بك لتسليم الرسائل. + توجيه الرسائل الخاصة + التوجيه الخاص + غير محمي + السماح بالرجوع إلى إصدار سابق + لا تستخدم التوجيه الخاص. + وضع توجيه الرسائل + لا + عندما يكون IP مخفيًا + لا ترسل رسائل مباشرةً، حتى لو كان خادمك أو خادم الوجهة لا يدعم التوجيه الخاص. + أرسل الرسائل مباشرة عندما لا يدعم الخادم الوجهة الخاص بك أو الخادم الوجهة التوجيه الخاص. + احتياطي توجيه الرسالة + أظهِر حالة الرسالة + احمِ عنوان IP + بدون تور أو VPN، سيكون عنوان IP الخاص بك مرئيًا لخوادم الملفات. + استخدم التوجيه الخاص مع خوادم غير معروفة. + استخدم التوجيه الخاص مع خوادم غير معروفة عندما لا يكون عنوان IP محميًا. + دائمًا + استخدم دائمًا التوجيه الخاص. + الملفات + مطلقًا + سيطلب التطبيق تأكيد التنزيلات من خوادم ملفات غير معروفة (باستثناء .onion أو عند تفعيل وكيل SOCKS). + أرسل الرسائل مباشرة عندما يكون عنوان IP محميًا ولا يدعم الخادم الوجهة لديك التوجيه الخاص. + خوادم غير معروفة + خوادم غير معروفة! + بدون تور أو VPN، سيكون عنوان IP الخاص بك مرئيًا لمُرحلات XFTP هذه: +\n%1$s. + أظهِر قائمة الدردشة في نافذة جديدة + ألوان الدردشة + سمة الدردشة + تلقيت رد + أزِل الصورة + تكرار + صفّر اللون + أرسلت رد + تعيين السمة الافتراضية + النظام + لون تمييز خلفية الشاشة + لون إضافي ثانوي 2 + الإعدادات المتقدمة + جميع أوضاع الألوان + أسود + وضع اللون + داكن + الوضع الداكن + ألوان الوضع الداكن + ملائمة + طاب يومك! + صباح الخير! + صورة خلفية الشاشة + الوضع الفاتح + سمة ملف التعريف + فاتح + طبّق لِ + ملء + المقياس + لا شيء + توجيه الرسائل الخاصة 🚀 + اجعل محادثاتك تبدو مختلفة! + تلقي الملفات بأمان + واجهة المستخدم الفارسية + صفّر إلى سمة التطبيق + سمة التطبيق + تأكيد الملفات من خوادم غير معروفة. + صفّر إلى سمة المستخدم + معلومات قائمة انتظار الخادم: %1$s +\n +\nآخر رسالة تم استلامها: %2$s + تسليم التصحيح + معلومات قائمة انتظار الرسائل + احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات اتصالك. \nفعّل في إعدادات *الشبكة والخوادم*. + سمات دردشة جديدة + حدث خطأ أثناء تهيئة WebView. حدّث نظامك إلى الإصدار الجديد. يُرجى التواصل بالمطورين. +\nError: %s + تحسين تسليم الرسائل + مع انخفاض استخدام البطارية. + مفتاح خاطئ أو عنوان مجموعة الملف غير معروف - على الأرجح حُذف الملف. + خطأ في خادم الملفات: %1$s + خطأ في الملف + خطأ في الملف مؤقت + حالة الرسالة + لم يتم العثور على الملف - على الأرجح حُذف الملف أو إلغاؤه. + حالة الملف + حالة الملف: %s + حالة الرسالة: %s + خطأ في النسخ + تم استخدام هذا الرابط مع جهاز محمول آخر، يُرجى إنشاء رابط جديد على سطح المكتب. + يُرجى التحقق من اتصال الهاتف المحمول وسطح المكتب بنفس الشبكة المحلية، وأن جدار حماية سطح المكتب يسمح بالاتصال. +\nيُرجى مشاركة أي مشاكل أُخرى مع المطورين. + لا يمكن إرسال الرسالة + تفضيلات الدردشة المحدّدة تحظر هذه الرسالة. + التفاصيل + بدءًا من %s.\nجميع البيانات خاصة على جهازك. + أرسلت الإجمالي + الحجم + الملفات المرفوعة + يُرجى المحاولة لاحقا. + خطأ في التوجيه الخاص + عنوان الخادم غير متوافق مع إعدادات الشبكة: %1$s. + إصدار الخادم غير متوافق مع تطبيقك: %1$s. + العضو غير نشط + رسالة محوّلة + لا يوجد اتصال مباشر حتى الآن، يتم تحويل من قِبل المُدير. + امسح / ألصِق الرابط + خوادم SMP المهيأة + خوادم SMP أخرى + خوادم XFTP المهيأة + خوادم XFTP أخرى + أظهِر النسبة المئوية + مُعطَّل + مستقرّ + يتوفر تحديث: %s + التمس التحديثات + نزّل %s (%s) + ثُبّت بنجاح + افتح مكان الملف + يُرجى إعادة تشغيل التطبيق. + تذكر لاحقا + تخطي هذه النسخة + أُلغيت تنزيل التحديث + مُعطَّل + غير نشط + معلومات الخوادم + عرض المعلومات ل + الأخطاء + الرسائل المُرسلة + الإجمالي + الخوادم المتصلة سابقًا + حدث خطأ أثناء إعادة الاتصال بالخادم + أعِد توصيل الخادم؟ + أعِد التوصيل بالخادم لفرض تسليم الرسالة. يستخدم حركة مرور إضافية. + صفّر جميع الإحصائيات + صفّر جميع الإحصائيات؟ + نُزّلت + الرسائل المُستلمة + الرسائل المُرسلة + سيتم تصفير إحصائيات الخوادم - لا يمكن التراجع عن هذا! + رُفع + صفّر + بدءًا من %s. + خادم SMP + خادم XFTP + معترف به + حُذفت القطع + نُزّلت القطع + اكتملت + الاتصالات + أُنشئت + أخطاء فك التعمية + حُذفت + الملفات التي نُزّلت + أخطاء التنزيل + منتهية الصلاحيّة + افتح إعدادات الخادم + أخرى + موّكل + مؤمن + أرسل الأخطاء + أُرسلت مباشرةً + مُرسَل عبر الوكيل + مشترك + أخطاء الاشتراك + رفع الأخطاء + التمس التحديثات + أخطاء معترف بها + نُزّل تحديث التطبيق + جميع ملفات التعريف + المحاولات + تجريبي + رُفع القطع + متصل + الخوادم المتصلة + جارِ الاتصال + الاتصالات النشطة + ملف التعريف الحالي + أخطاء الحذف + إحصائيات مفصلة + عطّل + خطأ في تصفير الإحصائيات + التكرارات + خطأ + حدث خطأ أثناء إعادة الاتصال بالخوادم + جارٍ تنزيل تحديث التطبيق، لا تغلق التطبيق + الملفات + حجم الخط + ثبّت التحديث + قد يتم تسليم الرسالة لاحقًا إذا أصبح العضو نشطًا. + الرسائل المُستلمة + استقبال الرسائل + لا توجد معلومات، حاول إعادة التحميل + أخطاء أخرى + قيد الانتظار + أعِد التوصيل + أعِد توصيل كافة الخوادم المتصلة لفرض تسليم الرسالة. يستخدم حركة مرور إضافية. + خوادم موّكلة + تلقي الأخطاء + تلقى الإجمالي + أعِد توصيل جميع الخوادم + أعِد توصيل الخوادم؟ + عنوان الخادم + الإحصائيات + تم تجاهل الاشتراكات + جلسات النقل + لكي يتم إعلامك بالإصدارات الجديدة، شغّل الفحص الدوري للإصدارات المستقرة أو التجريبية. + أنت غير متصل بهذه الخوادم. يتم استخدام التوجيه الخاص لتسليم الرسائل إليهم. + قرّب + حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقا. + عنوان خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. + عنوان خادم الوجهة %1$s غير متوافق مع إعدادات خادم التحويل %2$s. + إصدار الخادم الوجهة %1$s غير متوافق مع خادم التحويل %2$s. + فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقا. + إصدار خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. + مطفي + قوي + تمويه الوسائط + متوسط + ناعم + مكالمة + اتصل + مراسلة + افتح + بحث + الإعدادات + فيديو + تأكيد حذف جهة الاتصال؟ + حُذفت جهة الاتصال! + سيتم حذف جهة الاتصال - لا يمكن التراجع عن هذا! + حُذفت المحادثة! + احذف دون إشعار + أبقِ المحادثة + احذف المحادثة فقط + بإمكانك إرسال رسائل إلى %1$s من جهات الاتصال المؤرشفة. + ألصق الرابط + جهات اتصالك + شريط أدوات التطبيق القابلة للوصول + حُذفت جهة الاتصال. + السماح بالمكالمات؟ + أرسل رسالة لتفعيل المكالمات. + المكالمات ممنوعة! + لا يمكن مكالمة أحد أعضاء المجموعة + لا يمكن إرسال رسالة إلى عضو المجموعة + جارِ الاتصال بجهة الاتصال، يُرجى الانتظار أو التحقق لاحقًا! + جهات الاتصال المؤرشفة + ادعُ + لا توجد جهات اتصال مُصفاة + لا يمكن مكالمة جهة الاتصال + لا يزال بإمكانك عرض المحادثة مع %1$s في قائمة الدردشات. + يجب عليك السماح لجهات اتصالك بالاتصال حتى تتمكن من الاتصال بها. + يُرجى الطلب من جهة اتصالك تفعيل المكالمات. + حذف %d رسائل الأعضاء؟ + سيتم وضع علامة على الرسائل للحذف. سيتمكن المُستلم/(المُستلمون) من الكشف عن هذه الرسائل. + حدد + سيتم حذف الرسائل لجميع الأعضاء. + سيتم وضع علامة على الرسائل على أنها تحت الإشراف لجميع الأعضاء. + الرسالة + لا شيء محدد + محدّد %d + خوادم الوسائط والملفات + خوادم الرسائل + متابعة + وكيل SOCKS + يمكنك ترحيل قاعدة البيانات المُصدرة. + يمكنك حفظ الأرشيف المُصدر. + حالة الاتصال والخوادم. + تواصل مع أصدقائك بشكل أسرع + شريط أدوات الدردشة يمكن الوصول إليه + أرشفة جهات الاتصال للدردشة لاحقًا. + استخدم التطبيق بيد واحدة. + صُدرت قاعدة بيانات الدردشة + التحكم في شبكتك + حذف ما يصل إلى 20 رسالة في آن واحد. + لم يتم تصدير بعض الملفات + يحمي عنوان IP الخاص بك واتصالاتك. + اتصال TCP + حفظ وإعادة الاتصال + أنشئ + تجربة دردشة جديدة 🎉 + تمويه من أجل خصوصية أفضل. + كبّر حجم الخط + رسالة جديدة + ادعُ + خيارات الوسائط الجديدة + شغّل من قائمة الدردشة. + تبديل قائمة الدردشة: + يمكنك تغييره في إعدادات المظهر. + نزّل الإصدارات الجديدة من GitHub. + ترقية التطبيق تلقائيًا + صفّر كافة التلميحات + يُرجى التأكد من أن رابط SimpleX صحيح. + الرابط غير صالح + %1$d خطأ في الملف:\n%2$s + فشل تنزيل %1$d ملف/ات. + لم يتم تنزيل %1$d ملف/ات. + نزّل + شارك ملف التعريف + استخدم بيانات اعتماد الوكيل المختلفة لكل اتصال. + اسم المستخدم + قد يتم إرسال بيانات اعتمادك غير مُعمَّاة. + خطأ في حفظ الوكيل + إزالة الأرشيف؟ + وضع النظام + سيتم إزالة أرشيف قاعدة البيانات المرفوعة نهائيًا من الخوادم. + استخدم بيانات اعتماد الوكيل المختلفة لكل ملف تعريف. + استخدم بيانات اعتماد عشوائية + قاعدة بيانات الدردشة + حُذف %1$d ملف/ات. + لا يزال يتم تنزيل %1$d ملف/ات. + لا تستخدم بيانات الاعتماد مع الوكيل. + خطأ في تحويل الرسائل + خطأ في تبديل ملف التعريف + حدد ملف تعريف الدردشة + لقد تم نقل اتصالك إلى %s ولكن حدث خطأ غير متوقع أثناء إعادة توجيهك إلى ملف التعريف. + تحويل %1$s رسالة؟ + لم يحوّل %1$s من الرسائل + جارِ تحويل %1$s رسالة + حوّل الرسائل… + تحويل الرسائل بدون ملفات؟ + جارِ حفظ %1$s رسالة + تأكد من صحة تضبيط الوكيل. + %1$d خطأ في ملف آخر. + حُذفت الرسائل بعد تحديدها. + لا يوجد شيء لتحويله! + كلمة المرور + استيثاق الوكيل + سيتم حذف الرسائل - لا يمكن التراجع عن هذا! + الصوت مكتوم + حدث خطأ أثناء تهيئة WebView. تأكد من تثبيت WebView وأن بنيته المدعومة هي arm64.\nالخطأ: %s + شكل الرسالة + ذيل + ركن + جلسة التطبيق + الخادم + سيتم استخدام بيانات اعتماد SOCKS الجديدة في كل مرة تبدأ فيها تشغيل التطبيق. + سيتم استخدام بيانات اعتماد SOCKS الجديدة لكل خادم. + انقر فوق زر المعلومات الموجود بالقرب من حقل العنوان للسماح باستخدام الميكروفون. + افتح إعدادات Safari / مواقع الويب / الميكروفون، ثم اختر السماح لـ localhost. + لإجراء مكالمات، اسمح باستخدام الميكروفون. أنهِ المكالمة وحاول الاتصال مرة أخرى. + تجربة مستخدم أفضل + شكل الرسالة قابل للتخصيص. + تبديل الصوت والفيديو أثناء المكالمة. + حذف أو إشراف ما يصل إلى 200 رسالة. + حوّل ما يصل إلى 20 رسالة في آن واحد. + مكالمات أفضل + تواريخ أفضل للرسائل. + أمان أفضل ✅ + بروتوكولات SimpleX تمت مراجعتها بواسطة Trail of Bits. + تبديل ملف تعريف الدردشة لدعوات لمرة واحدة. + أخطاء في تضبيط الخوادم. + لملف تعريف الدردشة %s: + لا يوجد وسائط أو خوادم ملفات. + لا يوجد خوادم لإرسال الملفات. + لقد وصل الاتصال إلى الحد الأقصى من الرسائل غير المُسلمة، قد يكون جهة اتصالك غير متصلة بالإنترنت. + الرسائل غير المُسلَّمة + شارك رابطًا لمرة واحدة مع صديق + أمان الاتصال + لحماية الرابط الخاص بك من الاستبدال، يمكنك مقارنة رموز أمان جهات الاتصال. + خادم جديد + لوسائل التواصل الاجتماعي + أو للمشاركة بشكل خاص + إعدادات العنوان + أنشئ رابط لمرة واحدة + عنوان SimpleX أو رابط لمرة واحدة؟ + مُشغلي الشبكة + يمكنك تضبيط الخوادم عبر الإعدادات. + حدد مشغلي الشبكة الذين تريد استخدامهم. + يمكنك تضبيط المُشغلين في إعدادات الشبكة والخوادم. + حدّث + تابع + الشروط المتفق عليها + راجع الشروط + الخوادم المُعدة مسبقًا + سيتم قبول الشروط تلقائيًا للمُشغلين المفعّلين في: %s. + خوادمك + %s.]]> + %s.]]> + مُشغل الشبكة + المُشغل + %s خوادم + الموقع الإلكتروني + سيتم قبول الشروط في: %s. + قُبل الشروط في: %s. + استخدم %s + استخدم الخوادم + %s.]]> + شروط الاستخدام + للتوجيه الخاص + لتلقي + استخدم للملفات + اعرض الشروط + %s.]]> + %s، يجب قبول شروط الاستخدام.]]> + %s.]]> + أُضيفت خوادم الوسائط والملفات + الشروط المفتوحة + الخوادم الخاصة بالملفات الجديدة لملف دردشتك الحالي + لإرسال + خطأ في إضافة الخادم + خطأ في تحديث الخادم + التغييرات المفتوحة + خادم المُشغل + أُضيف الخادم إلى المُشغل %s. + تغيّر مُشغل الخادم. + أشرطة أدوات التطبيق + تمويه + الشفافية + فعّل flux في إعدادات الشبكة والخوادم لتحسين خصوصية البيانات الوصفية. + اللامركزية الشبكية + المُشغل المُعد مسبقًا الثاني في التطبيق! + لتحسين خصوصية البيانات الوصفية. + تحسين التنقل في الدردشة + اعرض الشروط المُحدثة + اقبل الشروط + أُضيفت خوادم الرسائل + عنوان أو رابط لمرة واحدة؟ + مع جهة اتصال واحدة فقط - المشاركة شخصيًا أو عبر أي مُراسل.]]> + سيتم قبول الشروط للمُشغلين المفعّلين بعد 30 يومًا. + مُشغلي الخادم + لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط: + خطأ في قبول الشروط + خطأ في حفظ الخوادم + على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسيقوم تطبيقك بتسليمها عبر خادم Flux. + لا يوجد خوادم لتوجيه الرسائل الخاصة. + لا يوجد خوادم رسائل. + لا يوجد خوادم لاستقبال الملفات. + لا توجد رسالة + لا يوجد خوادم لاستقبال الرسائل. + - فتح الدردشة عند أول رسالة غير مقروءة.\n- الانتقال إلى الرسائل المقتبسة. + يمكنك تعيين اسم الاتصال، لتذكر الأشخاص الذين تمت مشاركة الرابط معهم. + راجع لاحقًا + تغيّر بروتوكول الخادم. + شارك العنوان علناً + شارك عنوان SimpleX على وسائل التواصل الاجتماعي. + عنوان SimpleX والروابط لمرة واحدة آمنة للمشاركة عبر أي برنامج مُراسلة. + انقر فوق أنشئ عنوان SimpleX في القائمة لإنشائه لاحقًا. + حُذفت هذه الرسالة أو لم يتم استلامها بعد. + استخدم للرسائل + يحمي التطبيق خصوصيتك من خلال استخدام مُشغلين مختلفين في كل محادثة. + %s.]]> + %s.]]> + %s.]]> + عنوان العمل التجاري + يتم تشغيل التطبيق دائمًا في الخلفية + دردشات العمل التجاري + أضف أعضاء الفريق + أضف أصدقاء + أضف أعضاء فريقك إلى المحادثات. + يُحظر إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة. + أجهزة Xiaomi: يُرجى تفعيل التشغيل التلقائي (Autostart) في إعدادات النظام لكي تعمل الإشعارات.]]> + مُعمَّاة بين الطرفين، مع أمان ما بعد الكم في الرسائل المباشرة.]]> + تحقق من الرسائل كل 10 دقائق + يُمنع إرسال الرسائل المباشرة بين الأعضاء. + الدردشة + كيف يساعد على الخصوصية + سيتم حذف الدردشة لجميع الأعضاء - لا يمكن التراجع عن هذا! + سيتم حذف الدردشة لديك - لا يمكن التراجع عن هذا! + احذف الدردشة + الدردشة موجودة بالفعل! + حذف الدردشة؟ + %1$s.]]> + أو استورد ملف الأرشيف + لا توجد خدمة خلفية + الإشعارات والبطارية + فقط مالكي الدردشة يمكنهم تغيير التفضيلات. + الخصوصية لعملائك. + الجوالات عن بُعد + ادعُ للدردشة + مغادرة المجموعة؟ + سيتم إزالة العضو من الدردشة - لا يمكن التراجع عن هذا! + غادِر الدردشة + الرسالة كبيرة جدًا! + يُرجى تقليل حجم الرسالة وإرسالها مرة أخرى. + شريط أداة الدردشة القابلة للوصول + الدعوة قُبلت + طلبت الاتصال + يُرجى تقليل حجم الرسالة أو إزالة الوسائط ثم إرسالها مرة أخرى. + يمكنك نسخ الرسالة وتقليل حجمها لإرسالها. + عندما يتم تفعيل أكثر من مُشغل واحد، لن يكون لدى أي منهم بيانات تعريفية لمعرفة مَن يتواصل مع مَن. + سيتم تغيير الدور إلى %s. وسيتم إشعار الجميع في الدردشة. + سيتم إرسال ملف تعريفك للدردشة إلى أعضاء الدردشة + ستتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة. + عن المُشغلين + توصلت SimpleX Chat وFlux إلى اتفاق لتضمين الخوادم التي تديرها Flux في التطبيق. + جارِ إعادة التفاوض على التعمية. + إصلاح الاتصال؟ + يتطلب الاتصال إعادة التفاوض على التعمية. + إصلاح + فعّل السجلات + خطأ في حفظ قاعدة البيانات + شطب + الاتصال غير جاهز. + القائمة + لا دردشات + لا توجد محادثات في القائمة %s. + لا توجد محادثات غير مقروءة + لم يتم العثور على أي محادثات + المفضلات + أضف القائمة + الكل + المجموعات + افتح باستخدام %s + أضف إلى القائمة + احذف + حذف القائمة؟ + حرّر + اسم القائمة... + يجب أن يكون اسم القائمة والرموز التعبيرية مختلفين لجميع القوائم. + احفظ القائمة + جهات الاتصال + خطأ في إنشاء قائمة الدردشة + الشركات + خطأ في تحميل قوائم الدردشة + سيتم إزالة جميع المحادثات من القائمة %s، وسيتم حذف القائمة + أنشئ قائمة + خطأ في تحديث قائمة الدردشة + الملحوظات + تغيير القائمة + تغيير الترتيب + خطأ في حفظ الإعدادات + خطأ في إنشاء بلاغ + أنت والمشرفون فقط هم من يرون ذلك + بلاغ مؤرشف + لا يراه إلا المُرسِل والمُشرفين + أرشف + أرشف البلاغ + احذف البلاغ + بلّغ + ملف تعريفي غير لائق + إزعاج (spam) + سبب آخر + البلاغات + 1 بلاغ + %d بلاغات + بلاغات الأعضاء + بلّغ عن المحتوى: سيراه مشرفو المجموعة فقط. + بلّغ عن أُخرى: سيراه مشرفو المجموعة فقط. + مشرف + بلاغ مؤرشف بواسطة %s + بلّغ عن ملف تعريف العضو: سيراه مشرفو المجموعة فقط. + انتهاك إرشادات المجتمع + محتوى غير لائق + بلّغ عن مخالفة: سيراه مشرفو المجموعة فقط. + بلّغ عن إزعاج (spam): سيراه مشرفو المجموعة فقط. + أرشفة البلاغ؟ + سبب الإبلاغ؟ + سيتم أرشفة البلاغ لك. + إزعاج (spam) + نعم + اسأل + لا + افتح الرابط + فتح رابط الويب؟ + حُظر الاتصال + افتح الروابط من قائمة الدردشة + المحتوى ينتهك شروط الاستخدام + حُظر الاتصال بواسطة مُشغل الخادم:\n%1$s. + حُظر الملف بواسطة مُشغل الخادم:\n%1$s. + الافتراضي %s + سنة واحدة + عطّل حذف الرسائل التلقائي + تعطيل حذف الرسائل التلقائي؟ + لن يتم حذف الرسائل الموجودة في هذه الدردشة أبدًا. + احذف رسائل الدردشة من جهازك. + عيّن اسم الدردشة… + لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المُرسلة والمُستلمة في هذه الدردشة قبل التاريخ المحدّد. + تغيير حذف الرسائل التلقائي؟ + استخدم منفذ TCP %1$s عندما لا يتم تحديد أي منفذ. + استخدم منفذ الويب + منفذ TCP للمُراسلة + اكتم الكل + ذّكورات غير مقروءة + يمكنك ذكر ما يصل إلى %1$s من الأعضاء في الرسالة الواحدة! + السماح بالإبلاغ عن الرسائل إلى المشرفين. + منع الإبلاغ عن الرسائل للمشرفين. + أرشفة كافة البلاغات؟ + أرشف البلاغات + لكل المشرفين + لي + بلاغ: %s + يمكن للأعضاء الإبلاغ عن الرسائل إلى المشرفين. + سيتم أرشفة كافة البلاغات لك. + أرشفة %d بلاغ؟ + يُمنع الإبلاغ عن الرسائل في هذه المجموعة. + لا تفوت رسائل مهمة. + مساعدة المُدراء على إشراف مجموعاتهم. + أذكر الأعضاء 👋 + نظّم الدردشات في القوائم + أرسل بلاغات خاصة + اضبط انتهاء صلاحية الرسالة في الدردشات. + حذف أسرع من المجموعات. + أسماء ملفات الوسائط خاصة. + استلم إشعارًا عند ذكرك. + أداء مجموعات أفضل + خصوصية وأمان أفضل + إرسال أسرع للرسائل. + رُفض + رُفض + خطأ في قراءة عبارة مرور قاعدة البيانات + ينتظر + حُدثت الشروط + إزالة الأعضاء؟ + سيتم إخفاء جميع الرسائل الجديدة من هؤلاء الأعضاء! + سيتم إزالة الأعضاء من الدردشة - لا يمكن التراجع عن هذا! + إلغاء حظر الأعضاء للجميع؟ + حظر الأعضاء للجميع؟ + سيتم عرض رسائل من هؤلاء الأعضاء! + لا يمكن قراءة عبارة المرور في Keystore، يُرجى إدخالها يدويًا. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطورين. + سيتم إزالة الأعضاء من المجموعة - لا يمكن التراجع عن هذا! + المشرفين + لا يمكن قراءة عبارة المرور في Keystore. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطورين. + موافقة الانتظار + ضبّط مُشغلي الخادم + سياسة الخصوصية وشروط الاستخدام. + لا يمكن الوصول إلى الدردشات الخاصة والمجموعات وجهات اتصالك لمشغلي الخادم. + باستخدام SimpleX Chat، توافق على:\n- إرسال المحتوى القانوني فقط في المجموعات العامة.\n- احترام المستخدمين الآخرين – لا سبام. + اقبل + استخدم روابط قصيرة (تجريبي) + يتطلب هذا الرابط إصدار تطبيق أحدث. يُرجى ترقية التطبيق أو اطلب من جهة اتصالك إرسال رابط متوافق. + رابط كامل + رابط قصير + رابط قناة SimpleX + رابط اتصال غير مدعوم + استخدم منفذ TCP 443 للخوادم المُعدة مسبقًا فقط. + إيقاف التشغيل + الخوادم المُعدة مسبقًا + جميع الخوادم + 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 6ca14aace2..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,6 +13,8 @@ You will connect to all group members. Connect Connect incognito + Invalid link + Please check that SimpleX link is correct. Opening database… @@ -35,6 +37,10 @@ %d messages marked deleted moderated by %s %1$d messages moderated by %2$s + Only you and moderators see it + Only sender and moderators see it + archived report + archived report by %s blocked blocked by admin %d messages blocked @@ -69,6 +75,8 @@ connection %1$d connection established invited to connect + requested to connect + accepted invitation connecting… you shared one-time link you shared one-time link incognito @@ -83,12 +91,21 @@ SimpleX contact address SimpleX one-time invitation SimpleX group link + SimpleX channel link via %1$s SimpleX links Description Full link Via browser Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red. + Use short links (BETA) + + + Spam + Inappropriate content + Community guidelines violation + Inappropriate profile + Another reason Error saving SMP servers @@ -107,26 +124,60 @@ Invalid display name! This display name is invalid. Please choose another name. Error switching profile! + Error saving servers + No message servers. + No servers to receive messages. + No servers for private message routing. + No media & file servers. + No servers to send files. + No servers to receive files. + For chat profile %s: + Errors in servers configuration. + Error accepting conditions + Spam + Content violates conditions of use Connection timeout Connection error Please check your network connection with %1$s and try again. + Server address is incompatible with network settings: %1$s. + Server version is incompatible with your app: %1$s. + Private routing error + Error connecting to forwarding server %1$s. Please try later. + Forwarding server address is incompatible with network settings: %1$s. + Forwarding server version is incompatible with network settings: %1$s. + Forwarding server %1$s failed to connect to destination server %2$s. Please try later. + Destination server address of %1$s is incompatible with forwarding server %2$s settings. + Destination server version of %1$s is incompatible with forwarding server %2$s. + Please try later. Error sending message + Error forwarding messages Error creating message + Error creating report Error loading details Error adding member(s) Error joining group Cannot receive file Sender cancelled file transfer. + Unknown servers! + Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s. + %1$d other file error(s). Error receiving file + %1$d file error(s):\n%2$s Error creating address Contact already exists You are already connected to %1$s. Invalid connection link Please check that you used the correct link or ask your contact to send you another one. + Unsupported connection link + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Connection error (AUTH) Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection. + Connection blocked + Connection is blocked by server operator:\n%1$s. + Undelivered messages + The connection reached the limit of undelivered messages, your contact may be offline. Error accepting contact request Sender may have deleted the connection request. Error deleting contact @@ -157,14 +208,17 @@ Error updating user privacy Slow function Execution of function takes too long time: %1$d seconds: %2$s + Error updating chat list + Error creating chat list + Error loading chat lists Instant notifications Instant notifications! Instant notifications are disabled! - SimpleX background service – it uses a few percent of the battery per day.]]> + SimpleX runs in background instead of using push notifications.]]> It can be disabled via settings – notifications will still be shown while the app is running.]]> - allow SimpleX to run in background in the next dialog. Otherwise, the notifications will be disabled.]]> + Allow it in the next dialog to receive notifications instantly.]]> Battery optimization is active, turning off background service and periodic requests for new messages. You can re-enable them via settings. Periodic notifications Periodic notifications are disabled! @@ -181,6 +235,7 @@ To receive notifications, please, enter the database passphrase Can\'t initialize the database The database is not working correctly. Tap to learn more + Xiaomi devices: please enable Autostart in the system settings for notifications to work.]]> SimpleX Chat service @@ -248,14 +303,42 @@ Device authentication is disabled. Turning off SimpleX Lock. Stop chat Open chat console - Open chat profiles + Change chat profiles Open migration screen SimpleX Lock not enabled! You can turn on SimpleX Lock via Settings. Message delivery error + Message delivery warning Most likely this contact has deleted the connection with you. + No message + This message was deleted or not received yet. + Report reason? + Archive report? + Archive %d reports? + Archive all reports? + The report will be archived for you. + All reports will be archived for you. + For me + For all moderators + + + Error: %1$s + Wrong key or unknown connection - most likely this connection is deleted. + Capacity exceeded - recipient did not receive previously sent messages. + Network issues - message expired after many attempts to send it. + Destination server error: %1$s + Forwarding server: %1$s\nError: %2$s + Forwarding server: %1$s\nDestination server error: %2$s + Server address is incompatible with network settings. + Server version is incompatible with network settings. + + + Wrong key or unknown file chunk address - most likely file is deleted. + File is blocked by server operator:\n%1$s. + File not found - most likely file was deleted or cancelled. + File server error: %1$s Reply @@ -265,6 +348,10 @@ Edit Info Search + Archive + Archive report + Archive reports + Delete report Sent message Received message History @@ -282,14 +369,21 @@ Hide Allow Moderate + Report + Select Expand Delete message? Delete %d messages? Message will be deleted - this cannot be undone! + Messages will be deleted - this cannot be undone! Message will be marked for deletion. The recipient(s) will be able to reveal this message. + Messages will be marked for deletion. The recipient(s) will be able to reveal these messages. Delete member message? + Delete %d messages of members? The message will be deleted for all members. + The messages will be deleted for all members. The message will be marked as moderated for all members. + The messages will be marked as moderated for all members. Delete for me For everyone Stop file @@ -304,6 +398,12 @@ Revoke Forward Download + List + + Message forwarded + No direct connection yet, message is forwarded by admin. + Member inactive + Message may be delivered later if member becomes active. edited @@ -317,28 +417,62 @@ Welcome! This text is available in settings Chats + Settings connecting… send direct message you are invited to group join as %s + rejected connecting… Tap to start a new chat Chat with the developers You have no chats Loading chats… No filtered chats + No chats in list %s. + No unread chats + No chats + No chats found Tap to Connect Connect with %1$s? Search or paste SimpleX link + Tap Create SimpleX address in the menu to create it later. No selected chat + Nothing selected + Selected %d + Forward %1$s message(s)? + Nothing to forward! + Forward messages without files? + Messages were deleted after you selected them. + %1$d file(s) were not downloaded. + %1$d file(s) are still being downloaded. + %1$d file(s) failed to download. + %1$d file(s) were deleted. + Download + %1$s messages not forwarded + Favorites + Contacts + Groups + Businesses + Notes + Reports + Report: %s + All + Add list + 1 report + %d reports + Member reports Share message… Share media… Share file… Forward message… + Forward messages… + Cannot send message + Selected chat preferences prohibit this message. Attach @@ -353,14 +487,37 @@ The image cannot be decoded. Please, try a different image or contact developers. The video cannot be decoded. Please, try a different video or contact developers. you are observer - You can\'t send messages! - Please contact group admin. Files and media prohibited! Only group owners can enable files and media. Send direct message to connect + Forwarding %1$s messages + Saving %1$s messages SimpleX links not allowed Files and media not allowed Voice messages not allowed + Message + Message is too large! + Please reduce the message size and send again. + Please reduce the message size or remove media and send again. + You can copy and reduce the message size to send it. + Report spam: only group moderators will see it. + Report member profile: only group moderators will see it. + Report violation: only group moderators will see it. + Report content: only group moderators will see it. + Report other: only group moderators will see it. + + You can\'t send messages! + contact not ready + contact deleted + not synchronized + contact disabled + you are observer + Please contact group admin. + request to join rejected + group is deleted + removed from group + you left + can\'t send messages Image @@ -394,6 +551,9 @@ Error saving file Loading the file Please, wait while the file is being loaded from the linked mobile + File error + Temporary file error + Open with %s Voice message @@ -402,13 +562,35 @@ Notifications + Disable automatic message deletion? + Change automatic message deletion? + Messages in this chat will never be deleted. + This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + Disable delete messages + Delete chat messages from your device. + connect + open + message + call + search + video Delete contact? Contact and all messages will be deleted - this cannot be undone! + Contact will be deleted - this cannot be undone! + Keep conversation + Only delete conversation + Confirm contact deletion? Delete and notify contact + Delete without notification Delete contact + Conversation deleted! + You can send messages to %1$s from Archived contacts. + Contact deleted! + You can still view conversation with %1$s in the list of chats. Set contact name… + Set chat name… Connected Disconnected Error @@ -421,6 +603,10 @@ Renegotiate encryption? The encryption is working and the new encryption agreement is not required. It may result in connection errors! Renegotiate + Fix connection? + Connection requires encryption renegotiation. + Fix + Encryption renegotiation in progress. View security code Verify security code @@ -515,10 +701,24 @@ Mute + Mute all Unmute Favorite Unfavorite + Unread mentions + + Create list + Add to list + Change list + Save list + List name... + List name and emoji should be different for all lists. + Delete + Delete list? + All chats will be removed from the list %s, and the list deleted + Edit + Change order You invited a contact @@ -579,10 +779,19 @@ If you can\'t meet in person, show QR code in a video call, or share the link. + Share address publicly + Share SimpleX address on social media. You can share your address as a link or QR code - anybody can connect to you. You won\'t lose your contacts if you later delete your address. + Share 1-time link with a friend + with one contact only - share in person or via any messenger.]]> + You can set connection name, to remember who the link was shared with. + Connection security + SimpleX address and 1-time links are safe to share via any messenger. + To protect against your link being replaced, you can compare contact security codes. When people request to connect, you can accept or reject it. User Guide.]]> + Address or 1-time link? Connect via link @@ -593,11 +802,20 @@ New chat + New message Add contact + Scan / Paste link + Paste link One-time invitation link 1-time link SimpleX address Or show this code + Full link + Short link + Share profile + Select chat profile + Error switching profile + Your connection was moved to %s but an unexpected error occurred while redirecting you to the profile. Or scan QR code Keep unused invitation? You can view invitation link again in connection details. @@ -612,6 +830,10 @@ Invalid QR code The code you scanned is not a SimpleX link QR code. + Archived contacts + No filtered contacts + Your contacts + Scan code Incorrect security code! @@ -639,10 +861,13 @@ Send us email SimpleX Lock Chat console + Message servers SMP servers + Configured SMP servers + Other SMP servers Preset server address Add preset servers - Add server… + Add server Test server Test servers Save servers @@ -650,6 +875,7 @@ Some servers failed the test: Scan server QR code Enter server manually + New server Preset server Your server Your server address @@ -661,8 +887,13 @@ Delete server The servers for new connections of your current chat profile Save servers? + Media & file servers XFTP servers + Configured XFTP servers + Other XFTP servers + Show percentage Install SimpleX Chat for terminal + Reset all hints Star on GitHub Contribute Rate the app @@ -681,17 +912,27 @@ Save Network & servers Advanced network settings - Network settings + Advanced settings + SOCKS proxy SOCKS proxy settings Use SOCKS proxy + Proxy authentication + Use random credentials + Use different proxy credentials for each profile. + Use different proxy credentials for each connection. + Do not use credentials with proxy. + Your credentials may be sent unencrypted. + Username + Password port %d + Error saving proxy + Make sure proxy configuration is correct. Host Port Use SOCKS proxy? Access the servers via SOCKS proxy on port %d? Proxy must be started before enabling this option. Use direct Internet connection? If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to. - Update .onion hosts setting? Use .onion hosts When available No @@ -699,34 +940,83 @@ Onion hosts will be used when available. Onion hosts will not be used. Onion hosts will be required for connection.\nPlease note: you will not be able to connect to the servers without .onion address. - Onion hosts will be used when available. - Onion hosts will not be used. - Onion hosts will be required for connection. Transport isolation Chat profile + App session + Server Connection for each chat profile you have in the app.]]> + New SOCKS credentials will be used every time you start the app. + New SOCKS credentials will be used for each server. for each contact and group member.\nPlease note: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.]]> Update transport isolation mode? Use .onion hosts to No if SOCKS proxy does not support them.]]> Please note: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]> + Private routing + Always + Unknown servers + Unprotected + Never + Always use private routing. + Use private routing with unknown servers. + Use private routing with unknown servers when IP address is not protected. + Do NOT use private routing. + Message routing mode + Allow downgrade + Yes + When IP hidden + No + Send messages directly when your or destination server does not support private routing. + Send messages directly when IP address is protected and your or destination server does not support private routing. + Do NOT send messages directly, even if your or destination server does not support private routing. + Message routing fallback + Show message status + To protect your IP address, private routing uses your SMP servers to deliver messages. + TCP port for messaging + Use web port + Use TCP port %1$s when no port is specified. + Use TCP port 443 for preset servers only. + All servers + Preset servers + Off Appearance Customize theme - THEME COLORS + INTERFACE COLORS App version App version: v%s App build: %s Core version: v%s simplexmq: v%s (%2s) + Check for updates + Disabled + Stable + Beta + Update available: %s + Download %s (%s) + Skip this version + Downloading app update, don\'t close the app + App update is downloaded + Open file location + Install update + Installed successfully + Please restart the app. + Update download canceled + Remind later + Check for updates + To be notified about the new releases, turn on periodic check for Stable or Beta versions. + Disable + Show: Hide: Show developer options + Enable logs Database IDs and Transport isolation option. Developer options Show internal errors Show slow API calls Shutdown? Notifications will stop working until you re-launch the app + Error saving settings Create address @@ -751,12 +1041,20 @@ Invite friends Let\'s talk in SimpleX Chat Hi!\nConnect to me via SimpleX Chat: %s + For social media + Or to share privately + SimpleX address or 1-time link? + Create 1-time link + Address settings + Business address + Add your team members to the conversations. Continue Don\'t create address You can create it later You can make it visible to your SimpleX contacts via Settings. + Invite Profile name: @@ -791,6 +1089,7 @@ Enter your name: Create Create profile + Create Invalid name! Correct name to %s? About SimpleX @@ -840,15 +1139,17 @@ Speaker Headphones Bluetooth + Error initializing WebView. Update your system to the new version. Please contact developers.\nError: %s + Error initializing WebView. Make sure you have WebView installed and it\'s supported architecture is arm64.\nError: %s - The next generation of private messaging + The future of messaging Privacy redefined - The 1st platform without any user identifiers – private by design. - Immune to spam and abuse - People can connect to you only via the links you share. + No user identifiers. + Immune to spam + You decide who can connect. Decentralized - Open-source protocol and code – anybody can run the servers. + Anybody can host servers. Create your profile Make a private connection Migrate from another device @@ -856,29 +1157,53 @@ How SimpleX works - if SimpleX has no user identifiers, how can it deliver messages?]]> - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - to receive the messages, your contacts – the servers you use to message them.]]> - 2-layer end-to-end encryption.]]> - Read more in our GitHub repository. + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Only client devices store user profiles, contacts, groups, and messages. + end-to-end encrypted, with post-quantum security in direct messages.]]> GitHub repository.]]> Use chat Private notifications - It can be changed later via settings. + How it affects battery When app is running Periodic Instant Best for battery. You will receive notifications only when the app is running (NO background service).]]> - Good for battery. Background service checks messages every 10 minutes. You may miss calls or urgent messages.]]> - Uses more battery! Background service always runs – notifications are shown as soon as messages are available.]]> + No background service + Good for battery. App checks messages every 10 minutes. You may miss calls or urgent messages.]]> + Check messages every 10 minutes + Uses more battery! App always runs in background – notifications are shown instantly.]]> + App always runs in background + Notifications and battery Setup database passphrase Random passphrase is stored in settings as plaintext.\nYou can change it later. Use random passphrase + + Private chats, groups and your contacts are not accessible to server operators. + By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam. + Privacy policy and conditions of use. + Accept + Configure server operators + Server operators + Network operators + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + The app protects your privacy by using different operators in each conversation. + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + About operators + Select network operators to use. + How it helps privacy + You can configure servers via settings. + Conditions will be accepted for enabled operators after 30 days. + You can configure operators in Network & servers settings. + Review later + Update + Continue + Incoming video call Incoming audio call @@ -893,6 +1218,9 @@ Call already ended! video call audio call + To make calls, allow to use your microphone. End the call and try to call again. + Click info button near address field to allow using microphone. + Open Safari Settings / Websites / Microphone, then choose Allow for localhost. Audio & video calls @@ -926,6 +1254,7 @@ Audio on Speaker off Speaker on + Sound muted Flip camera @@ -960,6 +1289,9 @@ Protect app screen Encrypt local files Auto-accept images + Protect IP address + The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled). + Without Tor or VPN, your IP address will be visible to file servers. Send link previews Show last messages Message draft @@ -1015,26 +1347,47 @@ Disable (keep group overrides) Enable for all groups Disable for all groups + Blur media + Off + Soft + Medium + Strong + Open links from chat list + Yes + No + Ask + Open web link? + Open link YOU SETTINGS + CHAT DATABASE HELP SUPPORT SIMPLEX CHAT APP DEVICE CHATS + FILES SEND DELIVERY RECEIPTS TO Restart Shutdown Developer tools Experimental features SOCKS PROXY + INTERFACE LANGUAGE APP ICON THEMES Profile images + Message shape + Corner + Tail + Chat theme + Profile theme + Chat colors MESSAGES AND FILES + PRIVATE MESSAGE ROUTING CALLS Network connection Incognito mode @@ -1044,6 +1397,7 @@ Your chat database RUN CHAT + Remote mobiles Chat is running Chat is stopped CHAT DATABASE @@ -1069,13 +1423,12 @@ Error importing chat database Chat database imported Restart the app to use imported chat database. - Some non-fatal errors occurred during import - you may see Chat console for more details. + Some non-fatal errors occurred during import: Delete chat profile? This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Chat database deleted Restart the app to create a new chat profile. 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. Files & media Delete files for all chat profiles Delete all files @@ -1087,7 +1440,9 @@ 1 day 1 week 1 month + 1 year %s second(s) + default (%s) Messages This setting applies to messages in your current chat profile Delete messages after @@ -1095,6 +1450,12 @@ This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. Delete messages Error changing setting + Chat database exported + You may save the exported archive. + You may migrate the exported database. + Some file(s) were not exported + Continue + Error saving database Save passphrase in Keystore @@ -1135,6 +1496,7 @@ Wrong database passphrase + Error reading database passphrase Encrypted database Database error Keychain error @@ -1156,12 +1518,19 @@ Please enter the previous password after restoring database backup. This action can not be undone. Restore Restore database error - 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. + 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. + 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. + 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. Database upgrade Database downgrade Incompatible database version Confirm database upgrades + Reachable app toolbars + Reachable chat toolbar + Toggle chat list: + You can change it in Appearance settings. Show console in new window + Show chat list in new window Invalid migration confirmation Upgrade and open chat Downgrade and open chat @@ -1176,14 +1545,6 @@ Start chat? Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. - - Chat archive - CHAT ARCHIVE - Save archive - Delete archive - Created on %1$s - Delete chat archive? - invitation to group %1$s Join group? @@ -1194,7 +1555,9 @@ You joined this group. Connecting to inviting group member. Leave Leave group? + Leave chat? 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. Invite members Group inactive Invitation expired! @@ -1283,15 +1646,19 @@ observer author member + moderator admin owner + rejected removed left group deleted unknown status invited + pending approval + pending connecting (introduced) connecting (introduction invitation) connecting (accepted) @@ -1311,6 +1678,7 @@ Initial role Expand role selection Invite to group + Invite to chat Skip inviting members Select contacts Contact checked @@ -1322,13 +1690,20 @@ Invite members + Add team members + Add friends %1$s MEMBERS you: %1$s Delete group + Delete chat Delete group? + Delete chat? Group will be deleted for all members - this cannot be undone! + Chat will be deleted for all members - this cannot be undone! Group will be deleted for you - this cannot be undone! + Chat will be deleted for you - this cannot be undone! Leave group + Leave chat Edit group profile Add welcome message Welcome message @@ -1345,6 +1720,7 @@ Error creating member contact Error sending invitation Only group owners can change group preferences. + Only chat owners can change preferences. Address Share address You can share this address with your contacts to let them connect with %s. @@ -1352,12 +1728,16 @@ disabled Receipts are disabled This group has over %1$d members, delivery receipts are not sent. + Invite FOR CONSOLE Local name Database ID + Debug delivery Record updated at + Message status + File status Sent at Created at Received at @@ -1366,6 +1746,8 @@ Disappears at Database ID: %d Record updated at: %s + Message status: %s + File status: %s Sent at: %s Created at: %s Received at: %s @@ -1381,43 +1763,70 @@ Remove member? + Remove members? Remove member Send direct message Member will be removed from group - this cannot be undone! + Members will be removed from group - this cannot be undone! + Member will be removed from chat - this cannot be undone! + Members will be removed from chat - this cannot be undone! Remove Remove member Block member? Block member Block Block member for all? + Block members for all? Block for all All new messages from %s will be hidden! + All new messages from these members will be hidden! Unblock member? Unblock member Unblock Unblock member for all? + Unblock members for all? Unblock for all Messages from %s will be shown! + Messages from these members will be shown! Blocked by admin blocked + disabled + inactive MEMBER Role Change role Change Switch Change group role? - The role will be changed to \"%s\". Everyone in the group will be notified. - The role will be changed to \"%s\". The member will receive a new invitation. + The role will be changed to "%s". Everyone in the group will be notified. + The role will be changed to "%s". Everyone in the chat will be notified. + The role will be changed to "%s". The member will receive a new invitation. Connect directly? Сonnection request will be sent to this group member. Error removing member Error changing role Error blocking member for all Group + Chat Connection direct indirect (%1$s) + Message queue info + none + server queue info: %1$s\n\nlast received msg: %2$s + + Can\'t call contact + Connecting to contact, please wait or check later! + Contact is deleted. + Allow calls? + You need to allow your contact to call to be able to call them. + Calls prohibited! + Please ask your contact to enable calls. + Can\'t call group member + Send message to enable calls. + Can\'t message group member + Connection not ready. Welcome message @@ -1448,6 +1857,7 @@ Enter group name: Group full name: Your chat profile will be sent to group members + Your chat profile will be sent to chat members Create group @@ -1455,7 +1865,59 @@ Save group profile Error saving group profile + + Preset servers + Review conditions + Accepted conditions + Conditions will be automatically accepted for enabled operators on: %s. + Your servers + %s.]]> + %s.]]> + + + Operator + %s servers + Network operator + Website + Conditions accepted on: %s. + Conditions will be accepted on: %s. + Use servers + Use %s + Current conditions text couldn\'t be loaded, you can review conditions via this link: + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + View conditions + Accept conditions + Conditions of use + Updated conditions + %s, accept conditions of use.]]> + Use for messages + To receive + For private routing + Added message servers + Use for files + To send + The servers for new files of your current chat profile + Added media & file servers + Open conditions + Open changes + + + Error updating server + Server protocol changed. + Server operator changed. + + + Operator server + Server added to operator %s. + Error adding server + + TCP connection Reset to defaults sec TCP connection timeout @@ -1465,8 +1927,8 @@ PING interval PING count Enable TCP keep-alive - Revert Save + Save and reconnect Update network settings? Updating settings will re-connect the client to all servers. Update @@ -1504,23 +1966,30 @@ When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. + System + Light + Dark System Light Dark SimpleX + Black System Theme + Color mode Dark theme - Save color + Dark mode colors Import theme Import theme error Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure. Export theme Reset colors + Reset color + App theme Accent Additional accent Secondary @@ -1528,8 +1997,37 @@ Background Menus & alerts Title + Additional accent 2 Sent message + Sent reply Received message + Received reply + Wallpaper background + Wallpaper accent + Remove image + Font size + Zoom + App toolbars + Transparency + Blur + System mode + + + Good afternoon! + Good morning! + Scale + Repeat + Fill + Fit + Advanced settings + Reset to app theme + Reset to user theme + Set default theme + Apply to + All color modes + Light mode + Dark mode + You allow @@ -1613,22 +2111,28 @@ Prohibit sending SimpleX links Send up to 100 last messages to new members. Do not send history to new members. - Group members can send disappearing messages. - Disappearing messages are prohibited in this group. - Group members can send direct messages. - Direct messages between members are prohibited in this group. - Group members can irreversibly delete sent messages. (24 hours) - Irreversible message deletion is prohibited in this group. - Group members can send voice messages. - Voice messages are prohibited in this group. - Group members can add message reactions. - Message reactions are prohibited in this group. - Group members can send files and media. - Files and media are prohibited in this group. - Group members can send SimpleX links. - SimpleX links are prohibited in this group. + Allow to report messsages to moderators. + Prohibit reporting messages to moderators. + Members can send disappearing messages. + Disappearing messages are prohibited. + Members can send direct messages. + Direct messages between members are prohibited. + Direct messages between members are prohibited in this group. + Direct messages between members are prohibited in this chat. + Members can irreversibly delete sent messages. (24 hours) + Irreversible message deletion is prohibited. + Members can send voice messages. + Voice messages are prohibited. + Members can add message reactions. + Message reactions are prohibited. + Members can send files and media. + Files and media are prohibited. + Members can send SimpleX links. + SimpleX links are prohibited. Up to 100 last messages are sent to new members. History is not sent to new members. + Members can report messsages to moderators. + Reporting messages is prohibited in this group. Delete after %d sec %ds @@ -1650,6 +2154,7 @@ offered %s: %2s cancelled %s all members + moderators admins owners Enabled for @@ -1697,7 +2202,7 @@ Audio and video calls Support bluetooth and other improvements. Group moderation - Now admins can:\n- delete members\' messages.\n- disable members (\"observer\" role) + Now admins can:\n- delete members\' messages.\n- disable members ("observer" role) Group welcome message Set the message shown to new members! Further reduced battery usage @@ -1776,6 +2281,61 @@ Network management More reliable network connection. Lithuanian UI + Private message routing 🚀 + Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings. + New chat themes + Make your chats look different! + Safely receive files + Confirm files from unknown servers. + Improved message delivery + With reduced battery usage. + Persian UI + New chat experience 🎉 + New media options + It protects your IP address and connections. + Archive contacts to chat later. + Reachable chat toolbar + Use the app with one hand. + Connect to your friends faster. + Delete up to 20 messages at once. + Play from the chat list. + Blur for better privacy. + Increase font size. + Upgrade app automatically + Download new versions from GitHub. + Control your network + Connection and servers status. + Better security ✅ + SimpleX protocols reviewed by Trail of Bits. + Better calls + Switch audio and video during the call. + Better user experience + Switch chat profile for 1-time invitations. + Customizable message shape. + Better message dates. + Forward up to 20 messages at once. + Delete or moderate up to 200 messages. + Network decentralization + The second preset operator in the app! + Enable Flux in Network & servers settings for better metadata privacy. + for better metadata privacy. + Improved chat navigation + - Open chat on the first unread message.\n- Jump to quoted messages. + Business chats + Privacy for your customers. + Mention members 👋 + Get notified when mentioned. + Send private reports + Help admins moderating their groups. + Organize chats into lists + Don\'t miss important messages. + Better privacy and security + Private media file names. + Set message expiration in chats. + Better groups performance + Faster sending messages. + Faster deletion of groups. + View updated conditions seconds @@ -1823,6 +2383,9 @@ Connection stopped %s with the reason: %s]]> Disconnected with the reason: %s + 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. + This link was used with another mobile device, please create a new link on the desktop. + Copy error Disconnect desktop? Only one device can work at the same time Use from desktop in mobile app and scan QR code.]]> @@ -1890,10 +2453,12 @@ Open group Repeat join request? Group already exists! + Chat already exists! %1$s.]]> Already joining the group! You are already joining the group via this link. %1$s.]]> + %1$s.]]> Connect via link? @@ -1908,6 +2473,7 @@ Or paste archive link Paste archive link Invalid link + Or import archive file Migrating Preparing download Downloading link details @@ -1951,6 +2517,8 @@ Creating archive link Cancel migration Finalize migration + Remove archive? + The uploaded database archive will be permanently removed from the servers. Migrate from another device on the new device and scan QR code.]]> Or securely share this file link Delete database from this device @@ -1972,4 +2540,88 @@ WiFi Wired ethernet Other + + + Servers info + Files + No info, try to reload + Showing info for + All profiles + Current profile + Transport sessions + Connected + Connecting + Errors + Statistics + Messages sent + Messages received + Details + Starting from %s.\nAll data is kept private on your device. + Message reception + Active connections + Pending + Total + Connected servers + Previously connected servers + Proxied servers + You are not connected to these servers. Private routing is used to deliver messages to them. + Reconnect servers? + Reconnect all connected servers to force message delivery. It uses additional traffic. + Reconnect server? + Reconnect server to force message delivery. It uses additional traffic. + Error reconnecting servers + Error reconnecting server + Error + Reconnect all servers + Reset all statistics + Reset all statistics? + Servers statistics will be reset - this cannot be undone! + Reset + Error resetting statistics + Uploaded + Downloaded + Detailed statistics + Sent messages + Sent total + Received messages + Received total + Receive errors + Starting from %s. + SMP server + XFTP server + Reconnect + attempts + Sent directly + Sent via proxy + Proxied + Send errors + expired + other + duplicates + decryption errors + other errors + Acknowledged + Acknowledgement errors + Connections + Created + Secured + Completed + Deleted + Deletion errors + Subscribed + Subscriptions ignored + Subscription errors + Uploaded files + Size + Chunks uploaded + Upload errors + Chunks deleted + Chunks downloaded + Downloaded files + Download errors + Server address + Open server settings + + + You can mention up to %1$s members per message! \ 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 c087e0d896..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 @@ %1$s иска да се свърже с вас чрез 1 минута Добави сървъри чрез сканиране на QR кодове. - Добави сървър… + Добави сървър админ Добави съобщение при посрещане Всички данни се изтриват при въвеждане. @@ -89,8 +89,7 @@ за всеки чат профил, който имате в приложението.]]> аудио разговор Най-добро за батерията. Ще получавате известия само когато приложението работи (БЕЗ фонова услуга).]]> - Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) за всеки контакт и член на група. -\nМоля, обърнете внимание: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят. + за всеки контакт и член на група. \nМоля, обърнете внимание: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят.]]> Помолен да получи изображението Аудио и видео разговори аудио разговор (не е e2e криптиран) @@ -221,14 +220,13 @@ Грешка при свързване (AUTH) свързване… Промени адреса за получаване - Базата данни на чата е изтрита + Базата данни е изтрита Чатът работи Чатът е спрян - БАЗА ДАННИ НА ЧАТА - Базата данни на чат е импортирана + БАЗА ДАННИ + Базата данни е импортирана Потвърди новата парола… Потвърди актуализаациите на базата данни - Архив на чата свързан промяна на адреса… промяна на адреса за %s… @@ -317,7 +315,6 @@ Промени режима на самоунищожение Промени кода за достъп за самоунищожение ЧАТОВЕ - АРХИВ НА ЧАТА промяна на адреса… В момента максималният поддържан размер на файла е %1$s. ID в базата данни @@ -341,10 +338,7 @@ Понижаване на версията на базата данни Актуализация на базата данни версията на базата данни е по-нова от приложението, но няма миграция надолу за: %s - Създаден на %1$s - Изтрий архив - Изтриване на архива на чата\? - групата изтрита + групата е изтрита Контактът е проверен създател Създай групов линк @@ -412,7 +406,7 @@ Идентификатори в базата данни и опция за изолация на транспорта. Изтрий адрес Изтрий адрес\? - ЦВЕТОВЕ НА ТЕМАТА + ЦВЕТОВЕ НА ИНТЕРФЕЙСА Създай Създай профил Изтрий изображение @@ -446,7 +440,7 @@ Изтрий сървър Не създавай адрес Име на профила: - двуслойно криптиране от край до край.]]> + Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения. Деактивирай потвърждениeто\? Активиране (запазване на промените) Активирай потвърждениeто\? @@ -466,7 +460,7 @@ Изчезващи съобщения активирано активирано за контакт - Личните съобщения между членовете са забранени в тази група. + Личните съобщения между членовете са забранени в тази група. Различни имена, аватари и транспортна изолация. Оправяне на криптирането след възстановяване от резервни копия. Потвърждениe за доставка! @@ -482,7 +476,7 @@ Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили. Изпращане на потвърждениe за доставка Можете да активирате по-късно през Настройки - Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението. + Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението. Предупреждение: Може да загубите някои данни! Въведи правилна парола. активирано за вас @@ -497,7 +491,7 @@ Изтрий съобщенията Грешка при криптиране на базата данни Активиране на автоматично изтриване на съобщения\? - Вашата чат база данни не е криптирана - задайте парола, за да я защитите. + Вашата база данни не е криптирана - задайте парола, за да я защитите. Криптиране на база данни\? покана за група %1$s Изпратихте покана за групата @@ -517,7 +511,7 @@ криптирането е съгласувано за %s Редактирай групов профил Изчезва в: %s - Ролята ще бъде променена на \"%s\". Членът ще получи нова покана. + Ролята ще бъде променена на "%s". Членът ще получи нова покана. директна Предоговори криптирането Въведи име на групата: @@ -573,13 +567,13 @@ Криптирана база данни Активирай TCP keep-alive Изчезващите съобщения са забранени в този чат. - Изчезващите съобщения са забранени в тази група. + Изчезващите съобщения са забранени. Изчезващи съобщения Въведи съобщение при посрещане…(незадължително) Съобщение при посрещане Грешка при свързване със сървъра С незадължително съобщение при посрещане. - Грешка при изтриване на чат базата данни + Грешка при изтриване на базата данни Грешка при промяна на настройката Грешка при създаване на групов линк Грешка при изтриване на групов линк @@ -603,10 +597,10 @@ Предстояща връзка със сървъра Вашият контакт трябва да бъде онлайн, за да осъществите връзката. \nМожете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк). - Грешка при експортиране на чат базата данни + Грешка при експортиране на базата данни Грешка: %s Грешка при запазване на файл - Грешка при импортиране на чат базата данни + Грешка при импортиране на базата данни Грешка при отстраняване на член Грешка при запазване на профила на групата Грешка при зареждане на SMP сървъри @@ -647,14 +641,14 @@ Файлът ще бъде получен, когато вашият контакт завърши качването му. Файлът ще бъде получен, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно! Филтрирайте непрочетените и любимите чатове. - Членовете на групата могат да изпращат лични съобщения. + Членовете могат да изпращат лични съобщения. помощ ПОМОЩ Здравей, \nСвържи се с мен през SimpleX Chat: %s - Членовете на групата могат да добавят реакции към съобщенията. - Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) - Членовете на групата могат да изпращат гласови съобщения. + Членовете могат да добавят реакции към съобщенията. + Членовете могат необратимо да изтриват изпратените съобщения. (24 часа) + Членовете могат да изпращат гласови съобщения. Дори когато е деактивиран в разговора. Бързо и без чакане, докато подателят е онлайн! Френски интерфейс @@ -674,8 +668,8 @@ Груповият профил се съхранява на устройствата на членовете, а не на сървърите. Скрий Забрани изпращането на изчезващи съобщения. - Файловете и медията са забранени в тази група. - Членовете на групата могат да изпращат файлове и медия. + Файловете и медията са забранени. + Членовете могат да изпращат файлове и медия. Скрити чат профили Допълнително намален разход на батерията Групово модериране @@ -699,7 +693,7 @@ Само вие можете да изпращате изчезващи съобщения. Само вашият контакт може да изпраща изчезващи съобщения. Забрани изпращането на изчезващи съобщения. - Членовете на групата могат да изпращат изчезващи съобщения. + Членовете могат да изпращат изчезващи съобщения. Невалиден QR код Невалиден линк! Неправилен код за сигурност! @@ -711,7 +705,7 @@ Покани приятели Скрий профила Как се използва форматирането - Може да се промени по-късно през настройките. + Как се отразява на батерията Незабавно Режим инкогнито Покани членове @@ -722,14 +716,14 @@ Покажи профила Как се използва курсив - Импортиране на чат база данни\? + Импортиране на база данни? Ако изберете да отхвърлите, подателят НЯМА да бъде уведомен. Изображение Изображението ще бъде получено, когато вашият контакт завърши качването му. Изображението е запазено в Галерия Изображението е изпратено Изображението ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно! - Необратимото изтриване на съобщения е забранено в тази група. + Необратимото изтриване на съобщения е забранено. Подобрена конфигурация на сървъра Това може да се случи, когато: \n1. Времето за пазене на съобщенията е изтекло - в изпращащия клиент е 2 дена а на сървъра е 30. @@ -758,7 +752,7 @@ невалиден формат на съобщението Невалиден линк за връзка Скриване на контакт и съобщение - разрешете на SimpleX да работи във фонов режим в следващия диалогов прозорец. В противен случай известията ще бъдат деактивирани.]]> + Разрешете в следващия диалогов прозорец, за да получавате незабавно известия.]]> Незабавни известия Незабавни известия! Незабавните известия са деактивирани! @@ -771,7 +765,7 @@ Инсталирай SimpleX Chat за терминал Как работи Как работи SimpleX - Защитен от спам и злоупотреби + Защитен от спам Игнорирай Покани членове Необратимото изтриване на съобщения е забранено в този чат. @@ -797,16 +791,14 @@ Оценете приложението Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани. Мрежа и сървъри - Мрежови настройки + Разширени настройки Порт порт %d - Няма се използват Onion хостове. Задължително Не За свързване ще са необходими Onion хостове. \nМоля, обърнете внимание: няма да можете да се свържете със сървърите без .onion адрес. Ще се използват Onion хостове, когато са налични. - Ще се използват Onion хостове, когато са налични. Няма се използват Onion хостове. Нека да поговорим в SimpleX Chat Парола за показване @@ -860,7 +852,7 @@ Забрани реакциите на съобщенията. Моля, въведете предишната парола след възстановяване на резервното копие на базата данни. Това действие не може да бъде отменено. Голям файл! - Реакциите на съобщения са забранени в тази група. + Реакциите на съобщения са забранени. Ново в %s предлага %s предлага %s: %2s @@ -878,7 +870,6 @@ Няма избран чат Известия Известията ще спрат да работят, докато не стартирате отново приложението - За свързване ще са необходими Onion хостове. Само 10 изображения могат да бъдат изпратени едновременно Само собствениците на групата могат да активират файлове и медията. Само собствениците на групата могат да активират гласови съобщения. @@ -893,7 +884,7 @@ Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн. Нов архив на база данни Стар архив на база данни - Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности. + Някои не-фатални грешки са възникнали по време на импортиране: Съобщения Моля, въведете правилната текуща парола. Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите. @@ -921,7 +912,7 @@ Членът ще бъде премахнат от групата - това не може да бъде отменено! няма текст Острани член - Ролята ще бъде променена на \"%s\". Всички в групата ще бъдат уведомени. + Ролята ще бъде променена на "%s". Всички в групата ще бъдат уведомени. Само данни за локален профил Профилни и сървърни връзки Без звук @@ -939,7 +930,7 @@ Защитете чат профилите с парола! Сега администраторите могат: \n- да изтриват съобщения на членове. -\n- да деактивират членове (роля \"наблюдател\") +\n- да деактивират членове (роля "наблюдател") Очаквайте скоро още подобрения! Полски интерфейс Реакции на съобщения @@ -964,7 +955,7 @@ Получаване на съобщения… Работи, когато приложението е отворено Simplex Chat услуга - SimpleX фонова услуга – използва няколко процента от батерията на ден.]]> + SimpleX работи във фонов режим вместо да използва push известия.]]> За да получавате известия, моля, въведете паролата на базата данни Влезте с вашите идентификационни данни Грешка при доставката на съобщението @@ -972,7 +963,7 @@ Най-вероятно този контакт е изтрил връзката с вас. Няма история Отвори конзолата - Отвори чат профилите + Промени чат профилите Моля, запомнете я или я съхранявайте на сигурно място - няма начин да възстановите загубена парола! Получено съобщение Отговори @@ -987,19 +978,16 @@ Разрешение е отказано! профилно изображение запазено място за профилно изображение - Протокол и код с отворен код – всеки може да оперира собствени сървъри. - Хората могат да се свържат с вас само чрез ликовете, които споделяте. + Всеки може да оперира сървъри. + Вие решавате кой може да се свърже с вас. Поверителността преосмислена - Прочетете повече в нашето хранилище в GitHub. Добави поверителна връзка - ако SimpleX няма потребителски идентификатори, как може да доставя съобщения\?]]> Отвори Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес. Заключване след Режим на заключване Моля, докладвайте го на разработчиците. Защити екрана на приложението - Запази архив ЧЛЕН Премахване PING бройка @@ -1078,7 +1066,7 @@ Деактивиране на потвърждениe за доставка за групи\? Активиране за всички групи Изпращането на потвърждениe за доставка е разрешено за %d групи - Рестартирайте приложението, за да използвате импортирана чат база данни. + Рестартирайте приложението, за да използвате импортирана база данни. Тази група има над %1$d членове, потвърждениeто за доставка няма да се изпраща. СЪРВЪРИ %s: %s @@ -1145,12 +1133,9 @@ Запази настройките\? Високоговорителят е включен Високоговорителят е изключен - Спрете чата, за да активирате действията с базата данни. Роля - Отмени промените Запази Нулирай цветовете - Запази цвета Вторичен Избери За да започнете нов чат @@ -1160,7 +1145,7 @@ Този линк не е валиден линк за връзка! Вашите чат профили Сървърите за нови връзки на текущия ви чат профил - За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата \"Вашите чат профили\". + За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата "Вашите чат профили". Профилът се споделя само с вашите контакти. Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени. Докосни за активиране на профил. @@ -1188,14 +1173,14 @@ Напълно децентрализирана – видима е само за членовете. Транспортна изолация Благодарение на потребителите – допринесете през Weblate! - Хешът на предишното съобщение е различен. + Хешът на предишното съобщение е различен.\" Тествай сървърите Благодарение на потребителите – допринесете през Weblate! За да не се разкрива часовата зона, файловете с изображения/глас използват UTC. Този текст не е линк за връзка! Твърде много видеоклипове! Тази настройка се прилага за съобщения в текущия ви профил - За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти. + За да се защити поверителността, SimpleX използва идентификатори за опашки от съобщения, отделни за всеки от вашите контакти. Опит за свързване със сървъра, използван за получаване на съобщения от този контакт. Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s). Тестът е неуспешен на стъпка %s. @@ -1204,8 +1189,8 @@ Докосни бутона Благодарим Ви, че инсталирахте SimpleX Chat! Запази и уведоми контактите - Ново поколение поверителни съобщения - Първата платформа без никакви потребителски идентификатори – поверителна по дизайн. + Бъдещето на комуникацията + Няма потребителски идентификатори. Системна Неправилно ID на следващото съобщение (по-малко или еднакво с предишното). \nТова може да се случи поради някаква грешка или когато връзката е компрометирана. @@ -1217,7 +1202,7 @@ Ще бъдете свързани, когато устройството на вашия контакт е онлайн, моля, изчакайте или проверете по-късно! Няма да загубите контактите си, ако по-късно изтриете адреса си. Вашите настройки - Вашият SimpleX адрес + Вашият адрес в SimpleX Използвай за нови връзки Вашите XFTP сървъри Използвай сървърите на SimpleX Chat\? @@ -1226,13 +1211,11 @@ Вашите SMP сървъри Използвай SOCKS прокси Използвай SOCKS прокси\? - Актуализиране на настройката за .onion хостове\? Използване на директна интернет връзка\? Използвай .onion хостове Когато са налични Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство. Можете да използвате markdown за форматиране на съобщенията: - да получавате съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения.]]> Използвай чата Актуализация Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството. @@ -1250,7 +1233,7 @@ Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с %s. Премахни от любимите ВИЕ - Вашата чат база данни + Вашата база данни Изчаква се получаването на изображението Изчаква се получаването на изображението Изчаква се получаването на видеото @@ -1262,7 +1245,7 @@ Гласово съобщение Гласово съобщение (%1$s) Гласово съобщение… - Гласовите съобщения са забранени в тази група. + Гласовите съобщения са забранени. непрочетено Добре дошли! Добре дошли %1$s! @@ -1292,8 +1275,7 @@ Вашият чат профил ще бъде изпратен на членовете на групата Вашият чат профил ще бъде изпратен \nдо вашия контакт - Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. -\nТова действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени. + Вашата текуща база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. \nТова действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени. Актуализирането на настройките ще свърже отново клиента към всички сървъри. актуализиран профил на групата Видео @@ -1461,7 +1443,7 @@ Несъвместим! Изчаква се мобилното устройство да се свърже: Изтрий %d съобщения? - Свържи мобилно устройство + Свързване на мобилно устройство Свързване с %1$s? Премахни член Блокирай @@ -1647,12 +1629,12 @@ Потвърди мрежовите настройки Грешка при изтеглянето на архива Подготвя се качване - Грешка при експортиране на чат базата данни + Грешка при експортиране на базата данни Стартиране на чата Квантово устойчиво криптиране Миграция на данните от приложението Мигрирайте към друго устройство чрез QR код. - Обаждания \"картина в картина\" + Обаждания "картина в картина" По-безопасни групи Администраторите могат да блокират член за всички. Подробности за линка се изтеглят @@ -1728,8 +1710,8 @@ Мрежова връзка SimpleX линкове Забранете изпращането на SimpleX линкове - Членовете на групата могат да изпращат SimpleX линкове. - SimpleX линкове са забранени в тази група. + Членовете могат да изпращат SimpleX линкове. + SimpleX линкове са забранени. Активирано за собственици Камера @@ -1766,4 +1748,526 @@ Профилни изображения Променете формата на профилните изображения Квадрат, кръг или нещо между тях. - \ No newline at end of file + %1$d файл(а) не бяха изтеглени. + %1$d файл(а) все още се изтеглят. + Приети условия + Приеми условията + %1$d файлова грешка(и):\n%2$s + %1$d файл(а) бяха изтрити. + Неуспешно изтегляне на %1$d файл(а). + %1$s съобщения не са препратени + приета покана + %1$d друга(и) файлова(и) грешка(и). + a + b + За операторите + Допълнителен акцент 2 + Няма сървъри за получаване на файлове. + Грешка при поверително рутиране + Без Tor или VPN вашият IP адрес ще бъде видим за тези XFTP релета:\n%1$s. + Препращане на %1$s съобщението(ята)? + Препращане на съобщенията… + Препращане на %1$s съобщения + Позволи понижаване + Когато IP-то е скрито + НЕ изпращайте съобщения директно, дори ако вашият или получаващият сървър не поддържат поверително рутиране. + За поверителното рутиране + Грешка при свързване към препращащият сървър %1$s. Моля, опитайте по-късно. + Покажи състоянието на съобщението + За да защити вашия IP адрес, поверително рутиране използва вашите SMP сървъри за доставяне на съобщения. + Препращане на съобщенията без файловете? + Неизвестни сървъри + ФАЙЛОВЕ + Показване на списъка на чатовете в нов прозорец + Системна + Тъмна + Черна + Нулиране на цветовете + То защитава вашия IP адрес и връзки. + Адресът на препращащия сървър е несъвместим с мрежовите настройки: %1$s. + Версията на препращащия сървър е несъвместима с мрежовите настройки: %1$s. + Препращащият сървър %1$s не успя да се свърже с получаващия сървър %2$s. Моля, опитайте по-късно. + Версията на получаващия сървър %1$s е несъвместима с препращащия сървър %2$s. + Грешка от получаващия сървър: %1$s + Адресът на сървъра е несъвместим с мрежовите настройки. + Съобщението е препратено + Все още няма директна връзка, съобщението е препратено от администратора. + Тема на приложението + Защитете вашия IP адрес от реле сървърите за съобщения, избрани от вашите контакти.\nАктивирайте в настройките *Мрежа и сървъри*. + Поверително рутиране на съобщенията 🚀 + Препращайте до 20 съобщения наведнъж. + Не сте свързани с тези сървъри. Поверителното рутиране се използва за доставяне на съобщенията до тях. + Грешен ключ или неизвестна връзка - най-вероятно тази връзка е изтрита. + Капацитетът е надвишен - получателят не е получил предишно изпратените съобщения. + Препращащ сървър: %1$s\nГрешка: %2$s + Версията на сървъра е несъвместима с мрежовите настройки. + Защити IP адреса + ПОВЕРИТЕЛНО РУТИРАНЕ НА СЪОБЩЕНИЯ + Приложението ще поиска потвърждение за изтегляния от неизвестни файлови сървъри (с изключение на .onion сървъри или когато SOCKS прокси е активирано). + Грешка: %1$s + Изтегляне + Съобщенията бяха изтрити, след като ги избрахте. + Нищо за препращане! + Поверително рутиране + Незащитен + Изпращайте съобщения директно, когато IP адресът е защитен и вашият или получаващият сървър не поддържа поверително рутиране. + Изпращайте съобщения директно, когато вашият или получаващият сървър не поддържат поверително рутиране. + Винаги използвай поверително рутиране. + Използвай поверително рутиране с неизвестни сървъри. + Тема на профила + Цветен режим + Цветове за тъмен режим + Няма сървъри за поверително рутиране на съобщения. + Без Tor или VPN вашият IP адрес ще бъде видим за файловите сървъри. + Предупреждение за доставката на съобщението + НЕ използвайте поверително рутиране. + Цветове на чата + Грешка при препращане на съобщенията + Адресът на получаващия сървър %1$s е несъвместим с настройките на препращащия сървър %2$s. + Неизвестни сървъри! + Проблеми с мрежата - съобщението е отказано след много опити за изпращане. + Препращащ сървър: %1$s\nГрешка от получаващия сървър: %2$s + Може да копирате и намалите размера на съобщението, за да бъде изпратено. + Винаги + Не + Никога + Режим за рутиране на съобщения + Използвай поверително рутиране с неизвестни сървъри, когато IP адресът не е защитен. + Светла + Да + Тема на чата + Получен отговор + Изпратен отговор + Премахни изображението + Тапетен фон + Тапетен акцент + Добър ден! + Добро утро! + Запълване + Повтори + Мащаб + Моля, опитайте по-късно. + Файлът не е намерен - най-вероятно файлът е бил изтрит или отказан. + Грешка на файловия сървър: %1$s + Грешен ключ или неизвестен адрес на файлово парче - най-вероятно файлът е изтрит. + Файлов статус: %s + Всички профили + Започвайки от %s.\nВсички данни се съхраняват поверително на вашето устройство.. + Свържете отново сървъра, за да принудите доставката на съобщенията. Това използва допълнителен трафик. + Грешка при нулиране на статистиката + Сканирай / Постави линк + Конфигурирани XFTP сървъри + Размер на шрифта + Статус на съобщението: %s + Задай тема по подразбиране + Потвърдете файловете от неизвестни сървъри. + Изпратени съобщения + Грешка при стартиране на WebView. Актуализирайте системата си до новата версия. Моля, свържете се с разработчиците.\nГрешка: %s + Други XFTP сървъри + Мащабиране + Всички цветови режими + Светъл режим + С намален разход на батерията. + Файлова грешка + Статус на съобщението + Файлов статус + Нови чат теми + Безопасно получаване на файлове + Персийски потребителски интерфейс + Няма информация, опитайте да презаредите + Показване на информация за + Информация за сървърите + Файлове + Транспортни сесии + Грешка + Информация за опашката за съобщения + няма + информация за опашката на сървъра: %1$s\n\nпоследно получено съобщение: %2$s + Подробности + Повторно свързване на сървърите? + Предишни свързани сървъри + Грешка при повторното свързване на сървърите + Грешка при повторното свързване на сървъра + Повторно свързване на всички сървъри + Нулиране + Свалено + Избраните чат настройки забраняват това съобщение. + неактивен + Тъмен режим + Активни връзки + Предстоящи + Свързани сървъри + Общо + деактивирано + Свържете се отново с всички свързани сървъри, за да принудите доставката на съобщенията. Това използва допълнителен трафик. + Този линк е използван с друго мобилно устройство, моля, създайте нова линк на настолното устройство. + Свързан + Получени съобщения + Покритие на съобщенията + Сървъри през прокси + Нулиране на всички статистически данни + Нулиране на всички статистически данни? + Статистиката на сървърите ще бъде нулирана - това не може да бъде отменено! + Подробна статистика + Членът е неактивен + Съобщението може да бъде доставено по-късно, ако членът стане активен. + Не може да се изпрати съобщението + Покажи процент + Повторно свързване на сървъра? + Качено + Изпратени общо + Грешка при копиране + Адресът на сървъра е несъвместим с мрежовите настройки: %1$s. + Версията на сървъра е несъвместима с вашето приложение: %1$s. + Грешки + Статистика + Свързване + Изпратени съобщения + Отстраняване на грешки за доставка + Изпълване + Разширени настройки + Приложи към + Нулиране с темата за приложението + Нулиране с потребителска тема + Направете вашите чатове да изглеждат различно! + Текущ профил + Повторно свързване + Получени съобщения + Получени общо + Получени грешки + Моля, проверете дали мобилното и настолното устройство са свързани към една и съща локална мрежа и дали защитната стена на настолното устройство позволява връзката.\nМоля, споделете всички други проблеми с разработчиците. + Временна файлова грешка + Конфигурирани SMP сървъри + Други SMP сървъри + Подобрена доставка на съобщения + опити + изтекли + други + дубликати + Потвърден + Грешки при потвърждението + Връзки + Започвайки от %s. + XFTP сървър + Изпратени директно + Изпратени чрез прокси + Чрез прокси + SMP сървър + Изпрати грешки + други грешки + грешки при декриптиране + Грешки при изтриване + Създаден + Завършен + Изтрит + Защитен + Абониран + Абонаменти игнорирани + Абонаментни грешки + Качени парчета + Качени файлове + Грешки при качване + Размер + Парчета изтрити + Изтеглени парчета + Изтеглени файлове + Настройки + Деактивирано + Стабилен канал + Бета канал + Налична актуализация: %s + Изтегляне на актуализация на приложението, не затваряйте приложението + Създай + Много + Форма на съобщение + Позволи обаждания? + Обажданията са забранени! + Моля, поискайте вашия контакт да позволи обажданията. + Размазване за по-добра поверителност. + Изтрийте до 20 съобщения наведнъж. + Възпроизвеждане от чат списъка. + SimpleX протоколите, прегледани от Trail of Bits. + По-добра сигурност ✅ + %1$s съобщения се запазват + Запази разговора + Използвай различни прокси идентификационни данни за всяка връзка. + Грешка за запазване на прокси + Актуализацията на приложението е изтеглена + Обаждането на контакта не е позволено + Тече свързване с контакт, моля изчакайте или проверете по-късно! + Обаждането на груповия член е не е позволено + Системен режим + Архивирайте контактите, за да разговаряте по-късно. + Покани + Някои файлове не са били експортирани + Базата данни е експортирана + Невалиден линк + Моля, проверете, че SimpleX линкът е правилен. + Изтрий само разговора + SOCKS прокси + Автоматични актуализации на приложението + Запази и се свържи отново + Достъпен панел + Използвайте приложението с една ръка. + Свържете се с приятелите си по-бързо. + Контролирайте вашата мрежа + Изтеглете новите версии от GitHub. + Увеличете размера на шрифта. + По-добри обаждания + Превключете аудио и видео по време на разговора. + Няма филтрирани контакти + Вашите контакти + Можете да запазите експортирания архив. + Можете да мигрирате експортираната база данни. + Можете да го промените в настройките за Изглед. + TCP връзка + Състояние на връзката и сървърите. + Изтриване на %d съобщения на членовете? + Потвърди изтриването на контакта? + Потребителско име + Провери за актуализации + Изключено + Средно + Малко + Ъгъл + Опашка + Контактът е изтрит.. + Трябва да разрешите на вашия контакт да може да ви се обажда, за да можете и вие да се обаждате. + Постави линк + Връзката ви беше преместена към %s, но възникна неочаквана грешка при пренасочването ви към профила. + Сесия на приложението + Сървър + Изтегли %s (%s) + Пропусни тази версия + Провери за актуализации + БАЗА ДАННИ + Превключване на чат списъка: + Можете да изпращате съобщения до %1$s от архивираните контакти. + Достъпен панел + Изпращането на съобщения на груповия член не е налично + Грешка при стартиране на WebView. Уверете се, че сте инсталирали WebView и поддържаната архитектура е ARM64.\nГрешка: %s + Съобщенията ще бъдат изтрити - това не може да бъде отменено! + Съобщенията ще бъдат изтрити за всички членове. + Съобщенията ще бъдат маркирани като модерирани за всички членове. + Избрано %d + Нищо не е избрано + Съобщение + отвори + съобщение + обаждане + Контактът ще бъде изтрит - това не може да бъде отменено! + свързване + Изтрий без известие + Разговорът е изтрит! + Все още ще можете да видите разговора с %1$s в списъка с чатовете. + Сподели профил + Избери чат профил + Грешка при превключване на профил + Покани + Звукът е заглушен + Панели на приложението + Изпратете съобщение за да се активират обажданията. + търсене + видео + Контактът е изтрит! + Архивирани контакти + Достъпни панели + Отвори настройките на сървъра + Продължи + Отвори местоположението на файла + Инсталиране на актуализация + Напомни по-късно + Инсталирането е успешно + Моля, рестартирайте приложението. + Изтеглянето на актуализацията е отменено + Премахни архив? + Каченият архив на базата данни ще бъде окончателно премахнат от сървърите. + За да извършвате обаждания, разрешете да използва микрофона. Прекратете разговора и опитайте да се обадите отново. + Натиснете бутона за информация близо до адресното поле, за да разрешите използването на микрофона. + Отворете Safari Настройки / Уеб страници / Микрофон, след което изберете Позволи за localhost. + Нов интерфейс 🎉 + Нови медийни опции + Подобрен интерфейс + Избери + Съобщенията ще бъдат маркирани за изтриване. Получателят(ите) ще могат да разкрият тези съобщения. + Размазване на медия + Сървъри за съобщения + Ново съобщение + Сървъри за медия и файлове + Прокси идентификация + Използвай случайни идентификационни данни + Използвай различни прокси идентификационни данни за всеки профил. + Не използвай идентификационни данни с прокси. + Вашите идентификационни данни могат да бъдат изпратени некриптирани. + Парола + Уверете се, че прокси конфигурацията е правилна. + Нови идентификационни данни за SOCKS ще се използват всеки път, когато стартирате приложението. + Ще се използват нови SOCKS идентификационни данни за всеки сървър. + За да бъдете уведомени за новите версии, включете периодичната проверка за стабилни или бета версии. + Деактивиране + Нулирай всички съвети + Грешки при изтегляне + Сървърен адрес + Няма сървъри за съобщения. + Няма сървъри за получаване на съобщения. + За чат профил %s: + Грешки в конфигурацията на сървърите. + Недоставени съобщения + Връзката достигна лимита за недоставени съобщения, вашият контакт може да е офлайн. + Сподели SimpleX адресът в социалните мрежи. + Сигурност на връзката + За да се предпазите от подмяна на вашия линк, можете да сравните кодовете за сигурност на контакта. + За социалните мрежи + SimpleX адрес или еднократен линк? + Създай еднократен линк + Приложението защитава вашата поверителност, като използва различни оператори за всеки разговор. + Например, ако контактът ви получава съобщения чрез SimpleX Chat сървър, приложението ви ще ги достави чрез Flux сървър. + Сървърни оператори + Мрежови оператори + Преглед по-късно + Можете да конфигурирате операторите в Мрежа и сървъри. + Условията ще бъдат автоматично приети за активираните оператори на: %s. + Вашите сървъри + Условията ще бъдат приети на: %s. + Използвай сървърите + Използвай %s + %s.]]> + Преглед на условията + Условия за ползване + %s, приемете условията за използване.]]> + За изпращане + Добавени сървъри за съобщения + Използвай за файлове + - Отворете чата при първото непрочетено съобщение.\n- Преминете към цитирани съобщения. + Резервно рутиране на съобщения + Активирай логовете + Добавете членовете на вашия екип към разговорите. + задраскан + Продължи + Добави приятели + %s.]]> + Уеб страница + Условия, приети на: %s. + Дистанционни мобилни устройства + Предварително зададени сървъри + %s.]]> + Отвори промените + Грешка при актуализиране на сървъра + Сървърният протокол е променен. + Грешка при добавяне на сървър + Личните съобщения между членовете са забранени в този чат. + Персонализирана форма на съобщенията. + По-добри дати на съобщението. + Активиране на flux + за по-добра поверителност на метаданните. + Подобрена навигация в чата + Грешка при запазване на базата данни + Вашият профил ще бъде изпратен до членовете + %s.]]> + За получаване + Добавени медийни и файлови сървъри + %s.]]> + Връзката не е готова. + Оператор + Вижте актуализираните условия + Xiaomi устройства : моля, активирайте Autostart в системните настройки, за да работят известията.]]> + Няма съобщение + Или да се сподели лично + криптирани от край до край, с постквантова сигурност в директните съобщения.]]> + Без фонова услуга + Проверявай за съобщения на всеки 10 минути + Приложението винаги работи във фонов режим + Известия и батерия + Условията ще бъдат приети за активираните оператори след 30 дни. + Можете да конфигурирате сървърите през настройките. + Актуализация + %s.]]> + Използвай за съобщения + Личните съобщения между членовете са забранени. + %1$s.]]> + SimpleX адресът и еднократните линкове за връзки са безопасни за споделяне чрез всеки месинджър. + Можете да зададете име на връзка, за да запомните с кого е споделена. + Сървърът е добавен към оператор %s. + Грешка при актуализирането на чат списъка + Грешка при създаването на чат списъка + Това съобщение е изтрито или не е получено. + Грешка при зареждането на чат списъка + Няма чатове в списъка %s. + Няма непрочетени чатове + Докосни Създаване на SimpleX адрес в менюто, за да го създадете по-късно. + Няма чатове + Няма намерени чатове + Любими + Контакти + Групи + Добави списък + Отвори с %s + Поправи + В ход е предоговаряне на криптирането. + Създай списък + Добави към списъка + Запази списъка + Името на списъка и емотиконите трябва да са различни за всички списъци. + Всички чатове ще бъдат премахнати от списъка %s и списъкът ще бъде изтрит + Редактирай + само с един контакт - споделете лично или чрез произволен месинджър.]]> + Адрес или еднократен линк? + Нов сървър + %s сървъри + Текстът на текущите условия не може да бъде зареден, можете да прегледате условията през този линк: + Сървърният оператор е променен. + Покана за чат + Чатът ще бъде изтрит за вас - това не може да бъде отменено! + Напусни чата + Изтрий чата + Изтрий чата? + Чатът ще бъде изтрит за всички членове - това не може да бъде отменено! + Членът ще бъде премахнат от чата - това не може да бъде отменено! + Чат + Преглед на условията + Сървърите за нови файлове от текущия ви профил + Поверителност за вашите клиенти. + Децентрализация на мрежата + Вторият предварително зададен оператор в приложението! + Бизнес чатове + Сподели адресът публично + Всички + Само собствениците на чат могат да променят настройките. + Изтрий списъка? + Списък + Изтрий + Бизнеси + Име на списъка... + Когато е активиран повече от един оператор, никои от тях няма метаданни, за да научи кой с кого комуникира. + Чатът вече съществува! + Ролята ще бъде променена на %s. Всички в чата ще бъдат уведомени. + Мрежов оператор + Сървър на оператора + Прозрачност + Изтриване или модериране до 200 съобщения. + Или импортирайте архивен файл + заявка за свързване + Грешка при запазване на сървърите + Няма сървъри за медия и файлове. + Няма сървъри за изпращане на файлове. + Грешка при приемане на условия + Съобщението е твърде голямо! + Моля, намалете размера на съобщението и го изпратете отново. + Моля, намалете размера на съобщението или премахнете мултимедията и изпратете отново. + Бизнес адрес + Напусни чата? + Размазване + SimpleX Chat и Flux сключиха споразумение за включване на управлявани от Flux сървъри в приложението. + %s.]]> + %s.]]> + Отвори условията + Превключете чат профила за еднократен линк за връзка. + Поправи връзката? + Връзката изисква предоговаряне на криптирането. + Сподели еднократен линк за връзка с приятел + Настройки на адреса + Изберете мрежови оператори за използване. + Как помага за поверителността + Ще спрете да получавате съобщения от този чат. Историята на чата ще бъде запазена. + Добави членове на екипа + Заглушаване на всички + 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 @@ আনুষঙ্গিক রং অ্যাডমিনরা গ্রুপে যোগদানের সংযোগ-সূত্র তৈরি করতে পারবেন। QR কোড স্ক্যান করে সার্ভার যুক্ত করুন। - সার্ভার যুক্ত করুন… + সার্ভার যুক্ত করুন ঠিকানা ঠিকানা পরিবর্তন বাতিল করা হবে। বার্তা গ্রহণের পুরনো ঠিকানা ব্যবহার করা হবে। অ্যাপের সকল তথ্য মুছে ফেলা হয়েছে। 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 @@ + + + Afegiu servidors escanejant codis QR. + %1$s missatges no reenviats + Tots els missatges nous de %s s\'amagaran! + L\'aplicació només pot rebre notificacions quan s\'està executant, no s\'iniciarà cap servei en segon pla + El servei en segon pla sempre s\'executa: les notificacions es mostraran tan aviat com els missatges estiguin disponibles. + Color addicional + S\'ha demanat rebre la imatge + blocat + Tots els vostres contactes, converses i fitxers es xifraran de manera segura i es penjaran en trossos als relés XFTP configurats. + Sempre actiu + Identificador incorrecte de missatge + per a cada contacte i membre del grup.\nTingueu en compte: si teniu moltes connexions, el vostre el consum de bateria i trànsit pot ser substancialment més elevat i algunes connexions poden fallar.]]> + Hash incorrecte de missatge + 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ó. + intents + Blocar + %s té una versió no compatible. Si us plau, assegureu-vos que feu servir la mateixa versió als dos dispositius]]> + Trucada + L\'autenticació ha fallat + Autenticació no disponible + Tant tu com el teu contacte podeu enviar missatges de veu. + trucar + La trucada ja s\'ha acabat! + Es pot desactivar mitjançant la configuració: les notificacions encara es mostraran mentre l\'aplicació s\'executa.]]> + Color + El canvi d\'adreça s\'avortarà. S\'utilitzarà l\'adreça de recepció antiga. + Configuració avançada + Accepta automàticament les sol·licituds de contacte + Vols permetre els missatges de veu? + Mala adreça d\'ordinador + xifratge d\'extrem a extrem amb PFS, repudi i recuperació per intrusió.]]> + Tots els teus contactes romandran connectats. L\'actualització del perfil s\'enviarà als teus contactes. + Ja estem connectant! + Ja us uniu al grup! + Adjuntar + Identificador incorrecte de missatge + Cancel·la la vista prèvia del fitxer + S\'ha superat la capacitat: el destinatari no ha rebut missatges enviats anteriorment. + per a cada perfil de xat que tingueu a l\'aplicació.]]> + Afegeix un contacte: crea un enllaç d\'invitació nou o connecta\'t mitjançant un enllaç que has rebut.]]> + Tingueu en compte: utilitzar la mateixa base de dades en dos dispositius trencarà el desxifrat dels missatges de les vostres connexions, com a protecció de seguretat.]]> + Utilitza més bateria! L\'aplicació sempre s\'executa en segon pla: les notificacions es mostren a l\'instant.]]> + %s està en mal estat]]> + %s amb el motiu: %s]]> + %1$d missatge(s) moderat(s) per %2$s + blocat per l\'administrador + xifratge extrem a extrem resistent quàntic amb PFS, repudi i recuperació d\'intrusió.]]> + %1$d altre(s) error(s) del fitxer. + No es pot rebre el fitxer + Permet-ho al següent diàleg per rebre notificacions a l\'instant.]]> + No es pot inicialitzar la base de dades + Dispositius Xiaomi: activeu l\'inici automàtic a la configuració del sistema perquè les notificacions funcionin.]]> + Cancelar vista prèvia de la imatge + No es pot enviar el missatge + S\'ha demanat rebre el vídeo + Tornar + Càmera + Càmera no disponible + Cancelar + Cancel·la el missatge en directe + a dalt, aleshores: + Crea grup: per crear un grup nou.]]> + Escaneja el codi QR.]]> + Obre a l\'aplicació per a mòbils i després toca Connecta a l\'aplicació.]]> + Tots els missatges se suprimiran; això no es pot desfer! + Tots els missatges s\'eliminaran NOMÉS per a tu; això no es pot desfer! + cancel·la la vista prèvia de l\'enllaç + Es compartirà un nou perfil aleatori. + només amb un contacte: compartiu-lo en persona o mitjançant qualsevol missatger.]]> + escanejar el codi QR a la videotrucada o el vostre contacte pot compartir un enllaç d\'invitació.]]> + mostra el codi QR a la videotrucada o comparteix l\'enllaç.]]> + Contactes arxivats + Guia de l\'usuari.]]> + Configuració de xarxa avançada + Configuració avançada + Accedir als servidors mitjançant el servidor SOCKS al port %d? S\'ha d\'iniciar abans d\'activar aquesta opció. + per sessió + Tingueu en compte: 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.]]> + Permetre la versió anterior + Sempre + Utilitzeu sempre l\'encaminament privat. + Creació de l\'aplicació: %s + Aparença + S\'ha baixat l\'actualització de l\'aplicació + Versió + Versió de l\'aplicació: v%s + Beta + Tots els teus contactes romandran connectats. + Afegeix els membres del teu equip a les converses. + Acceptació automàtica + Adreça d\'empresa + negreta + trucada acabada %1$s + error de trucada + trucant… + trucada en curs + Càmera + Càmera i micròfon + Qualsevol pot allotjar servidors. + Bluetooth + xifrats d\'extrem a extrem, amb seguretat postquàntica als missatges directes.]]> + El millor per a la bateria. Només rebràs notificacions quan l\'aplicació s\'està executant (Sense servei en segon pla).]]> + Bo per a la bateria. L\'aplicació revisa els missatges cada 10 minuts. Podeu perdre trucades o missatges urgents.]]> + repositori GitHub.]]> + %1$s vol connectar amb tu mitjançant + Feu servir sempre el retransmisor + App always runs in background + trucada + trucada (no xifrada) + Trucades d\'àudio i vídeo + Trucades a la pantalla de bloqueig: + %1$d missatge(s) omés(os). + %1$d missatge(s) omés(os). + Respondre trucada + Àudio desactivat + Àudio activat + hash de missatge erroni + La trucada ha finalitzat + Trucada en curs + Còpia de seguretat de dades de l\'aplicació + Contrasenya de l\'aplicació + S\'ha cancel·lat l\'autenticació + Accepta imatges automàticament + S\'eliminaran totes les dades de l\'aplicació. + Es crea un perfil de xat buit amb el nom proporcionat i l\'aplicació s\'obre com de costum. + La contrasenya de l\'aplicació es substitueix per una contrasenya d\'autodestrucció. + APLICACIÓ + ICONA APLICACIÓ + Desenfocar els mitjans + TRUCADES + Android Keystore s\'utilitza per emmagatzemar de manera segura la frase de contrasenya: permet que el servei de notificacions funcioni. + 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. + No es pot accedir a Keystore per desar la contrasenya de la base de dades + Tingueu en compte: NO podreu recuperar ni canviar la contrasenya si la perdeu.]]> + No es poden convidar contactes! + ha blocat a %s + i %d esdeveniments més + acordant el xifratge… + acordant el xifratge per a %s… + autor + %1$s MEMBRES + No es pot convidar el contacte! + Tots els membres del grup romandran connectats. + Blocar per a tots + Blocar membre + Blocar membre? + Blocar membre per a tots? + Blocat per l\'administrador + No es pot trucar al contacte + Permetre trucades? + Trucades prohibides! + No es pot trucar al membre del grup + No es pot enviar missatges als membres del grup + %s.]]> + %s.]]> + %s.]]> + %s.]]> + S\'han afegit servidors multimèdia i de fitxers + S\'han afegit servidors de missatges + Tots els xats i missatges se suprimiran; això no es pot desfer! + Negre + Tema de l\'aplicació + Secundari addicional + Fons + Color addicional 2 + Tots els modes + sempre + Aplicar a + Barres d\'eines d\'aplicació + Desenfocar + Trucades d\'àudio/vídeo + \nDisponible en v5.1 + Permet trucades només si el vostre contacte ho permet. + Permet la desaparició de missatges només si el vostre contacte ho permet. + "Permet la supressió irreversible del missatge només si el teu contacte t\'ho permet. (24 hores)" + Permet les reaccions als missatges només si el teu contacte les permet. + Permet missatges de veu només si el vostre contacte els permet. + Permet que els teus contactes afegeixin reaccions als missatges. + Permet que els teus contactes et truquin. + Permet als teus contactes eliminar de manera irreversible els missatges enviats. (24 hores) + Permet que els teus contactes enviïn missatges que desapareixen. + Permet que els teus contactes enviïn missatges de veu. + Tant vos com els vostres contactes podeu suprimir de manera irreversible els missatges enviats. (24 hores) + Tant tu com el teu contacte podeu enviar missatges que desapareguin. + Tant tu com el teu contacte podeu afegir reaccions als missatges. + Permet la supressió irreversible del missatge només si el teu contacte t\'ho permet. (24 hores) + Permet enviar missatges directes als membres. + Permeteu suprimir de manera irreversible els missatges enviats. (24 hores) + Permet enviar missatges que desapareixen. + Permet enviar fitxers i mitjans. + Permet enviar missatges de veu. + Les trucades d\'àudio/vídeo estan prohibides. + Tant tu com el teu contacte podeu fer trucades. + Permet enviar enllaços SimpleX. + tots els membres + cancel·lat %s + Els administradors poden crear els enllaços per unir-se als grups. + Trucades d\'àudio i vídeo + Per perfil de xat (per defecte) o per connexió (BETA). + Totes les dades s\'esborren quan s\'introdueix. + Contrasenya de l\'aplicació + Missatges millorats + Unes quantes coses més + Xifratge dels fitxers locals nous (excepte vídeos). + Àrab, búlgar, finès, hebreu, tailandès i ucraïnès - gràcies als usuaris i Weblate. + Grups millorats + Blocar membres del grup + Migració de dades d\'aplicacions + Arxiva els contactes per xatejar més tard. + Desenfoca per a una millor privadesa. + Trucades millorades + Dates de missatges millorades. + Seguretat millorada ✅ + Millor experiència d\'usuari + Xats d\'empresa + (aquest dispositiu v%s)]]> + (nou)]]> + %s s\'ha desconnectat]]> + Usar des de l\'ordinador a l\'aplicació mòbil i escaneja el codi QR.]]> + %s està ocupat]]> + %s està inactiu]]> + %s no es troba]]> + %s s\'ha desconnectat]]> + Aplicar + Arxivar i carregar + Arxivant bases de dades + Cancel·la la migració + Migra des d\'un altre dispositiu al dispositiu nou i escaneja el codi QR.]]> + Tots els perfils + Avís: l\'arxiu s\'eliminarà.]]> + Reconegut + Errors de reconeixement + %1$d fitxer(s) no s\'ha(n) baixat. + %1$d missatge(s) no s\'ha(n) pogut desencriptar. + s\'està connectant + blocat + k + Connecta + error + vostè + EN DIRECTE + moderat + reenviat + desat + s\'està connectant… + Descripció + %1$d error(s) de fitxer:\n%2$s + Desconnecta + Permet + Autentica + Desbloca + Comparteix + Desa + Edita + Info + Cerca + Reenviat + Esborra + Mostra + Amaga + Modera + Selecciona + Expandeix + La baixada ha fallat per a %1$d fitxer(s). + %1$d fitxer(s) ha(n) estat eliminat(s). + esborrat + Ocult + Còpia + Lliurament + %1$d encara s\'estan baixant els fitxers. + Connecta + Error + Respon + Amaga + connectat + Atura + Connectat + Immediatament + Historial + Desat + Permet + Revoca + invitació acceptada + 30 segons + Accepta + Voleu acceptar la sol·licitud de connexió? + Acceptar incògnit + Quant a l\'adreça de SimpleX + Enllaç d\'un sol ús + Afegiu un contacte + Adreça o enllaç d\'un sol ús? + Afegiu servidors predefinits + Afegeix servidor + Quant a SimpleX + trucada acceptada + Accepta + 1 dia + 1 mes + 1 setmana + administrador + Afegiu-hi amics + Afegeix membres de l\'equip + Afegeix missatge de benvinguda + Adreça + Cancel·la el canvi d\'adreça + Accepta les condicions + Condicions acceptades + Afegiu un perfil + Voleu cancel·lar el canvi d\'adreça? + 1 minut + Accepta + 6 noves llengües d\'interfície + Cancel·la + Quant a SimpleX Chat + Els administradors poden blocar un membre per a tothom. + Connexions actives + 5 minuts + Accepta + Quant als operadors + 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. + Configuració d\'adreça + Afegeix a un altre dispositiu + administradors + Voleu suprimir el contacte? + Desapareixerà a: %s + L\'autenticació del dispositiu està desactivada. S\'està desactivant el bloqueig SimpleX. + L\'autenticació del dispositiu no està activada. Podeu activar SimpleX Lock mitjançant Configuració, un cop hàgiu activat l\'autenticació del dispositiu. + desactivat + %1$s.]]> + %1$s.]]> + L\'adreça del servidor de destinació de %1$s és incompatible amb la configuració del servidor de reenviament %2$s. + Suprimeix el perfil + %d hores + Esborrar per mi + Els missatges directes entre membres estan prohibits en aquest xat. + La versió del servidor de destinació de %1$s és incompatible amb el servidor de reenviament %2$s. + La migració de la base de dades està en curs. Pot trigar uns minuts. + %d missatge(s) bloquejat(s) + %d missatge(s) bloquejat(s) per l\'administrador + %d missatge(s) marcat(s) eliminat(s) + Error de desxifrat + Suprimeix la cua + Esborrar fitxer + SimpleX s\'executa en segon pla en lloc d\'utilitzar notificacions push.]]> + Desactivar notificacions + Ús de la bateria de l\'aplicació / Sense restriccions a la configuració de l\'aplicació.]]> + Ús de la bateria de l\'aplicació / Sense restriccions a la configuració de l\'aplicació.]]> + %d minuts + Desactiva el bloqueig SimpleX + Error del servidor de destinació: %1$s + Voleu suprimir el missatge? + Voleu suprimir els %d missatges? + Voleu suprimir %d missatges dels membres? + Vols suprimir el missatge del membre? + Error de descodificació + Eliminar i notificar el contacte + Suprimeix el contacte + Suprimeix sense notificació + Desconnectat + Missatge que desapareix + Suprimeix + Suprimeix + Voleu suprimir la connexió pendent? + Contrasenya i exportació de la base de dades + Suprimeix el servidor + Utilitza els hosts .onion a No si el servidor SOCKS no els admet.]]> + Desactivat + NO envieu missatges directament, encara que el vostre servidor de destinació no admeti l\'encaminament privat. + Voleu suprimir l\'adreça? + Opcions de desenvolupador + Desactivar + Esborrar adreça + Suprimeix la imatge + El nom mostrat no pot contenir espais en blanc. + Descentralitzada + Desactivar + Desactivar per a tothom + Desactiva per a tots els grups + Desactivar (conservant anul·lacions) + Desactivar (mantenint les anul·lacions del grup) + Desactivar rebuts? + Desactivar rebuts per a grups? + Eines per a desenvolupadors + DISPOSITIU + La base de dades es xifra amb una contrasenya aleatòria. Si us plau, canvieu-la abans d\'exportar. + Contrasenya de la base de dades + Voleu suprimir el perfil? + Esborrar la base de dades + Suprimeix tots els fitxers + Voleu suprimir els fitxers i els mitjans? + Suprimeix els fitxers de tots els perfils + %d fitxer(s) amb una mida total de %s + Base de dades xifrada! + Suprimir missatges + La base de dades es xifra amb una contrasenya aleatòria, podeu canviar-la. + La contrasenya de xifratge de la base de dades s\'actualitzarà. + La contrasenya de xifratge de la base de dades s\'actualitzarà i s\'emmagatzemarà a la configuració. + La contrasenya de la base de dades és diferent de la desada al Keystore. + La contrasenya de la base de dades és necessària per obrir el xat. + La base de dades es xifrarà i la contrasenya s\'emmagatzemarà a la configuració. + La base de dades es xifrarà. + La base de dades es xifrarà i la frase de contrasenya s\'emmagatzemarà a Keystore. + La contrasenya de xifratge de la base de dades s\'actualitzarà i s\'emmagatzemarà a Keystore. + Actualització de la base de dades + 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 + migració diferent a l\'aplicació/base de dades: %s / %s + contacte eliminat + grup esborrat + %d esdeveniments de grup + %d contacte(s) seleccionat(s) + Suprimir xat + Suprimir xat? + Suprimir grup + Suprimir grup? + Suprimir enllaç + Suprimir enllaç? + Entrega de depuració + Suprimit + Suprimit: %s + Desapareixerà a + directe + inactiu + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s, accepteu les condicions d\'ús.]]> + Voleu suprimir el perfil? + Suprimeix el perfil + predeterminat (%s) + Suprimeix per a tothom + Missatges directes + Missatges que desapareixen + Els missatges que desapareixen estan prohibits en aquest xat. + %dd + %d dia + %d dies + Suprimeix després + %dh + %d hora + Els missatges directes entre membres estan prohibits. + Els missatges directes entre membres estan prohibits en aquest grup. + Els missatges que desapareixen estan prohibits. + %dm + %d min + %d mes + %d mesos + %dmes + No envieu l\'historial als nous membres. + Missatges que desapareixen + Diferents noms, avatars i aïllament del transport. + Descobreix i uneix-te a grups + Suprimir fins a 20 missatges alhora. + dies + Suprimeix o modera fins a 200 missatges. + Rebuts de lliurament! + Els rebuts de lliurament estan desactivats! + La versió de l\'aplicació d\'ordinador %s no és compatible amb aquesta aplicació. + Ordinadors + Dispositius + Desconnectar + Desconnectar l\'ordinador? + S\'ha desconnectat amb el motiu: %s + Desconnectar mòbils + Ordinador + Adreça ordinador + L\'ordinador té una versió no compatible. Si us plau, assegureu-vos que feu servir la mateixa versió als dos dispositius + L\'ordinador té un codi d\'invitació incorrecte + L\'ordinador està ocupat + L\'ordinador està inactiu + L\'ordinador ha estat desconnectat + Descobrible a través de la xarxa local + Descobriu a través de la xarxa local + %s]]> + %1$s!]]> + %1$s.]]> + %1$s.]]> + Suprimir la base de dades d\'aquest dispositiu + No heu d\'utilitzar la mateixa base de dades en dos dispositius. + Detalls + errors de desxifrat + Estadístiques detallades + Suprimides + Errors d\'eliminació + Mòbil connectat + Connectar en mode incògnit + connexió %1$d + Comprova si hi ha missatges cada 10 minuts durant un minut com a màxim + Canvia el codi d\'accés + Canvia el perfil + Confirmeu les credencials + Xats + Parleu amb els desenvolupadors + connecta + Confirmeu l\'eliminació del contacte? + Connectat + Confirma + Voleu suprimir les notes privades? + Botó de tancament + Connecta + Consola del xat + Comproveu l\'adreça del servidor de i torneu a provar. + Servidors SMP configurats + Configura els servidors ICE + Perfil del xat + Connexió + Comprova si hi ha actualitzacions + Comprova si hi ha actualitzacions + connectat + Comprova missatges cada 10 minuts + Canvia la modalitat de bloqueig + Canvia el mode l\'autodestrucció + Canvia el codi d\'autodestrucció + Confirmeu el codi d\'accés + BASE DE DADES DELS XATS + XATS + Tema del xat + Colors del xat + Base de dades suprimida + Base de dades importada + Base de dades exportada + Confirmeu el nou mot de pas… + Voleu canviar la frase de pas per a la base de dades? + connectat + connectat directament + canviant d\'adreça per %s… + connectat + complet + Canvia + Xat + Condicions acceptades el: %s. + Condicions d\'ús + Mode de color + Preferències del xat + Compareu els codis de seguretat amb els vostres contactes. + Estat de la connexió i dels servidors + Connectat a un mòbil + Pròximament! + Connecta automàticament + El xat ja existeix! + Xat migrat! + Comproveu la connexió a Internet i torneu a provar + Servidors connectats + Blocs eliminats + Blocs descarregats + Error en desar el fitxer + Canviar l\'adreça de recepció? + connectar amb els desenvolupadors de SimpleX Chat per fer qualsevol pregunta i rebre actualitzacions.]]> + Obre a l\'aplicació mòbil.]]> + Error en desar els servidors ICE + Error en desar el servidor intermediari + BASE DE DADES DELS XATS + El xat s\'està executant + El xat està aturat + Error: %s + El xat està aturat + 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. + ha canviat el rol de %s a %s + ha canviat el teu rol a %s + ha canviat d\'adreça per a tu + canviant d\'adreça… + canviant d\'adreça… + El xat se suprimirà; això no es pot desfer! + Canviar el rol del grup? + Canviar rol + Canvia l\'adreça de recepció + Error en desar el perfil del grup + Mòbil + Errors + Contacte verificat + El contacte i tots els missatges s\'eliminaran, l\'acció és irreversible! + connexió establerta + Error de connexió + Temps de connexió enhaurit + El contacte la existeix + Error de connexió (AUTH) + Nom del contacte + Contacte eliminat! + El contacte encara no s\'hi ha connectat! + Sol·licitud de connexió tramesa! + Connecta via enllaç + el contacte admet criptografia d\'extrem a extrem + el contacte no té criptografía d\'extrem a extrem + El contacte ha estat eliminat + Preferències del contacte + Connecteu amb els vostres amic més ràpidament. + Connexió interrompuda + Connexió aturada + Connexió finalitzada + Voleu connectar a través d\'un enllaç? + Connexions + Error en carregar l\'arxiu + Error en desar la configuració + Error en aturar el xat + Error en actualitzar el servidor + Introduïu la contrasenya + Error en iniciar el xat + Error en desar la contrasenya de l\'usuari + Introduïu la contrasenya correcta. + Introduïu la contrasenya… + Introduïu el nom del grup: + Error en verificar la contrasenya: + finalitzada + Introduïu la contrasenya + error en mostrar el contingut + error en mostrar el missatge + Error en desar els servidors SMP + Error en desar els servidors XFTP + Error en enviar el missatge + Errors en la configuració dels servidors. + Error en canviar el perfil! + Error en actualitzar la configuració de la xarxa + Error en establir l\'adreça + Error en sincronitzar la connexió + Error en actualitzar la privadesa de l\'usuari + Error en mostrar la notificació, contacteu amb els desenvolupadors. + Introduïu el codi + Error en canviar el perfil + Introduïu el servidor manualment + Error en enviar la invitació + Error en actualitzar l\'enllaç del grup + Fins i tot quan està desactivat a la conversa. + Continua + Continua + Continua + Icona contextual + El contacte serà eliminat; l\'acció no es pot desfer! + Conversa eliminada! + Copiat al porta-retalls + Contribuïu + Contactes + Error en restablir les estadístiques + Error + Error en reconnectar els servidors + Introduïu el missatge de benvinguda… (opcional) + Introduïu el vostre nom: + Error en obrir el navegador + Error en exportar la base de dades de xat + S\'ha produït un error en suprimir la base de dades de xat + Error en importar la base de dades de xat + Error en canviar la configuració + Error en eliminar el membre + Error en bloquejar el membre per a tots + Error en canviar el rol + Error en afegir servidor + Error en suprimir la base de dades + Error en reconnectar el servidor + Error en xifrar la base de dades + Error en exportar la base de dades de xat + Blocs penjats + El xat se suprimirà per a tots els membres; això no es pot desfer! + acolorit + Feu clic al botó d\'informació al costat del camp d\'adreça per permetre l\'ús del micròfon. + Completades + Error en acceptar les condicions + Error en connectar-se al servidor de reenviament %1$s. Si us plau, prova-ho més tard. + Error en crear el missatge + Error en crear el perfil! + Error en reenviar els missatges + Error en carregar els servidors SMP + Error en carregar els servidors XFTP + Error en afegir membre(s) + Error en crear l\'adreça + Error en unir-se al grup + Error en carregar els detalls + Error en rebre el fitxer + Error en acceptar la sol·licitud de contacte + S\'ha produït un error en suprimir el contacte + S\'ha produït un error en suprimir el grup + Error en cancel·lar el canvi d\'adreça + Error en canviar l\'adreça + S\'ha produït un error en suprimir la sol·licitud de contacte + S\'ha produït un error en suprimir la connexió de contacte pendent + S\'ha produït un error en suprimir les notes privades + S\'ha produït un error en suprimir el perfil d\'usuari + Comparar el fitxer + Error: %1$s + Error + Trieu un fitxer + Netejar + Netejar + Netejar xat + Netejar xat? + Netejar verificació + Error en inicialitzar WebView. Assegureu-vos que teniu WebView instal·lat i que sigui suportada l\'arquitectura arm64. Error: %s + Error en inicialitzar WebView. Actualitzeu el vostre sistema a la nova versió. Poseu-vos en contacte amb els desenvolupadors. Error: %s + Netejar + Error en crear l\'enllaç del grup + Error en crear el contacte del membre + Error en suprimir l\'enllaç del grup + Introduïu el missatge de benvinguda… + Interfície en xinès i espanyol + Error en habilitar els rebuts de lliurament. + Introduïu el nom d\'aquest dispositiu… + Error + Error en baixar l\'arxiu + Crea una adreça + Crea + Crea un grup tot fent servir un perfil aleatori. + Perfil actual + Crea un enllaç d\'un sol ús + No crear cap adreça + Crea + Crea perfil + Cantonada + Frase de pas actual… + Crea un enllaç + Crea un grup secret + No tornis a mostrar + Tema fosc + Creat + Crea un grup + Crea un enllaç de group + Errada crítica + personalitzat + Mode fosc + Crea una cua + Crea un fitxer + Codi d\'accés actual + Ara per ara, la mida màxima per als fitxers és %1$s. + Crea un enllaç d\'invitació d\'un sol ús + Crea un grupo secreto + Temps personalitzat + Crea un perfil de xat + Creeu una adreça perquè la gent pugui connectar amb vós. + Crea una adreça SimpleX + Crea perfil + creador + ID de la base de dades + ID de la base de dades: %d + (actual) + Fosc + Fosc + Creeu el vostre perfil + editat + SimpleX Chat i Flux han acordat incloure els servidors operats per Flux a l\'aplicació. + Voleu compartir l\'adreça de SimpleX o un enllaç d\'un sol ús? + Configuració + Tanca + s\'està connectant… + Les condicions s\'acceptaran per als operadors habilitats després de 30 dies. + SimpleX no pot funcionar en segon pla. Només rebreu les notificacions quan obriu l\'aplicació. + Trucades de SimpleX chat + Missatges de xat de SimpleX + enviat + per llegir + Benvinguts! + Envia + Envia + Restableix + Fitxer + Imatge + Vídeo + Silencia + Activa el so + Favorit + Configuració + ajuda + Correu electrònic + Adreça de SimpleX + Més + Compartir els enllaços d\'un sol ús i les adreces de SimpleX és segur a través de qualsevol mitjà. + Enganxa + Desa + Torna-ho a provar + Adreça de SimpleX + Desa + Nom d\'usuari + Contrasenya + Amfitrió + Port + No + Requerit + Amb IP desprotegida + Mai + + No + Estable + Mostra: + Amaga: + SimpleX + La seguretat de SimpleX chat ha estat auditada per Trail of Bits. + Parlem a SimpleX Chat + El nom no és vàlid! + cursiva + Crea una connexió privada + Trucada d\'àudio entrant + Codi d\'accés incorrecte + Bloca després + Mode de blocatge + Mode d\'incògnit + inactiu + Mode clar + Grups d\'incògnit + MEMBRE + Voleu unir-vos al grup? + Surt + Voleu sortir del xat? + Voleu sortir del grup? + Invitació caducada! + Convida membres + convidat + ha sortit + Protegeix la vostra adreça IP i les vostres connexions. + So de trucada + Videotrucada entrant + indirecte (%1$s) + invitació al grup %1$s + Convida + Versió incompatible + Interfície en lituà + Error intern + Augmenta la mida de la lletra. + Nom local + Enllaç no vàlid + Convida + Convida amics + Interfície en italià + Ruta de fitxer no vàlida + dades no vàlides + En resposta a + Descarrega + Membre inactiu + Conserva la conversa + Marcar com ha llegit + Enllaç no vàlid + El codi QR no és vàlid + Més informació + El codi QR no és vàlid + Codi de seguretat incorrecte! + L\'adreça del servidor no és vàlida! + Ajuda sobre Markdown + Markdown en els missatges + Assegureu-vos que la configuració del servidor intermediari és correcta. + Instal·la l\'actualització + "Instal·lació completada" + Mitjà + La versió de la base de dades és incompatible + ha convidat %1$s + ha sortit + membre + Convida al grup + Surt del xat + Surt del grup + Incògnit + El mode d\'incògnit protegeix la vostra privacitat usant un perfil aleatori nou per a cada contacte. + Clar + Clar + Els membres poden enviar missatges directes. + Els membres poden enviar fitxers i multimèdia. + Corregir el nom a %s? + connectant… + Trucada amb xifratge d\'extrem a extrem + Videotrucada amb xifratge d\'extrem a extrem + xifrat d\'extrem a extrem + Activa les trucades des de la pantalla de bloqueig mitjançant Configuració. + missatge duplicat + Activa bloqueig + Activar (conservar anul·lacions) + Activa (mantenir les anul·lacions de grup) + Activar la supressió automàtica de missatges? + Creat a + Connectar-se directament? + Connexió + El text de les condicions actuals no s\'ha pogut carregar, podeu revisar les condicions mitjançant aquest enllaç: + Trucada en connexió + Baixant l\'arxiu + %d setmana + Confirmeu la contrasenya + connectant (presentat) + connectant (anunciat) + connectant (invitació de presentació) + %ds + %d setmanes + Baixa noves versions de GitHub. + Habiliteu Flux a la configuració de la xarxa i dels servidors per obtenir una millor privadesa de les metadades. + Connectar amb l\'ordinador + Connectant + Errors de descàrrega + Confirmeu fitxers de servidors desconeguts. + Les condicions s\'acceptaran el dia: %s. + Connectades + Arxius descarregats + Descarregat + connectant (acceptat) + Ordinador connectat + Connectat amb l\'ordinador + trucada en connexió… + Creant un enllaç a l\'arxiu + Connectant amb l\'ordinador + - Connexió al servei de directoris (BETA)!\n- Confirmacions de lliurament (fins a 20 membres).\n- Més ràpid i més estable. + Habilita per a tothom + el contacte %1$s ha canviat a %2$s + El contacte permet + %ds + duplicats + Editar imatge + Activar + Habilita per a tots els grups + Habilita als xats directes (BETA)! + Copiar error + La baixada ha fallat + %d seg + Connectar mitjançant l\'adreça de contacte? + Conectar mitjançant enllaç d\'invitació? + Nom de visualització duplicat! + Descarregar fitxer + Contacte ocult: + %d segons + connectant… + Connectar amb %1$s? + Descarregar + La connexió requereix una renegociació del xifratge. + Connectar mitjançant enllaç/codi QR + Habilita l\'accés a la càmera + Seguretat de connexió + Creant enllaç… + Servidors XFTP configurats + No utilitzeu credencials amb servidor intermediari. + Versió del nucli: v%s + Personalitzar tema + NO utilitzeu l\'encaminament privat. + S\'està baixant l\'actualització de l\'aplicació, no la tanquis + Baixa %s (%s) + ID de bases de dades i opció d\'aïllament de transport. + Auriculars + Error de base de dades + Confirmeu les actualitzacions de la base de dades + Reducció de versió de la base de dades + Baixa la versió anterior i obri el xat + connectant + Editar perfil del grup + Creat a: %s + Connectant al contacte, si us plau, espereu o comproveu-ho més tard! + Les condicions s\'acceptaran automàticament per als operadors habilitats el dia: %s. + Colors en mode fosc + habilitat per al contacte + habilitat + habilitat per a tu + Els contactes poden marcar missatges per suprimir-los. Encara els podreu veure. + Habilitat per + Personalitza i comparteix temes de color. + Temes personalitzats + Crea perfil nou a l\'aplicació per a ordinador. 💻 + Forma de missatge personalitzable. + No activeu + Connectar amb tu mateix? + Confirmeu la configuració de la xarxa + Confirmeu que recordeu la contrasenya de la base de dades per migrar-la. + La connexió a l\'ordinador és deficient + Confirmar la càrrega + Controla la teva xarxa + Baixant els detalls de l\'enllaç + Error en desar els servidors + Habilita els registres + Per a xarxes socials + Nom complet: + Sortir sense desar + Contrasenya de perfil amagada + Concedir permisos + Per exemple, si el vostre contacte rep missatges mitjançant un servidor SimpleX Chat, la vostra aplicació els lliurarà mitjançant un servidor Flux. + Gira la càmera + Activar codi d\'autodestrucció + Activar els rebuts? + Activar autodestrucció + Activar els rebuts per a grups? + FITXERS + EXPERIMENTAL + Exportar base de dades + Xifrar + Fitxer: %s + Grup + Arreglar connexió + Totalment descentralitzat: només visible per als membres. + Moderació de grups + Missatge de benvinguda als grups + Grup inactiu + La invitació de grup ja no és vàlida, el remitent l\'ha eliminada. + Grup no trobat! + es requereix renegociar el xifratge + grup esborrat + PER A CONSOLA + Arreglar connexió? + Correcció no suportada per membre del grup + Nom complet del grup: + Farciment + Encaix + Enllaços de grup + Perfils de xat ocults + Xifra els fitxers i els mitjans emmagatzemats + Ordinador trobat + El grup ja existeix! + Finalitzar la migració + expirats + xifratge acordat + Unió més ràpida i missatges més fiables. + Activar TCP keep-alive + S\'ha suprimit el fitxer o l\'enllaç no és vàlid + Error en desar la base de dades + xifratge ok + Penjar + Bon dia! + Preferències del grup + Fitxers + Correcció no suportada pel contacte + Arreglar + per a una millor privadesa de les metadades. + Bona tarda! + El fitxer exportat no existeix + Reenvia i desa missatges + Hola!\nConnecteu-vos amb mi mitjançant SimpleX Chat: %s + Característiques experimentals + La renegociació del xifratge ha fallat. + renegociació de xifratge permesa + Error de renegociació de xifratge + Enllaç complet + No s\'ha pogut carregar el xat + No s\'han pogut carregar els xats + Per al perfil de xat %s: + 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. + L\'adreça del servidor de reenviament és incompatible amb la configuració de xarxa: %1$s. + La versió del servidor de reenviament és incompatible amb la configuració de xarxa: %1$s. + L\'execució de la funció triga massa temps: %1$d segons: %2$s + Trucada finalitzada + Activar Bloqueig SimpleX + Servidor de reenviament: %1$s\nError: %2$s + Servidor de reenviament: %1$s\nError del servidor de destinació: %2$s + No s\'ha trobat el fitxer: el més probable és que s\'hagi suprimit o cancel·lat. + Error del servidor de fitxers: %1$s + Reenviat des de + Per a tothom + El fitxer s\'eliminarà dels servidors. + Reenviar + Reenviar %1$s missatges? + Reenviar missatges sense fitxers? + Reenviar missatge… + Reenviar missatges… + Arxius i mitjans prohibits! + Reenviant %1$s missatges + No es permeten fitxers ni suports + Fitxer + Error de fitxer + No s\'ha trobat el fitxer + Fitxer desat + El fitxer es rebrà quan el vostre contacte acabi de carregar-lo. + El fitxer es rebrà quan el vostre contacte estigui en línia, espereu o comproveu més tard! + Arreglar connexió? + Arreglar + Renegociació de xifratge en curs. + De la Galeria + Cerqueu aquest permís a la configuració d\'Android i concediu-lo manualment. + Concedir a la configuració + Donar permís(os) per fer trucades + Auriculars + Xifra fitxers locals + AJUT + Arxius i mitjans + Xifrar base de dades? + Base de dades xifrada + Invitació del grup caducada + perfil de grup actualitzat + xifratge acordat per a %s + xifratge correcte per a %s + renegociació de xifratge permesa per a %s + es requereix renegociar el xifratge per a %s + Amplieu la selecció de rols + El grup se suprimirà per a tots els membres; això no es pot desfer! + Enllaç de grup + El grup se suprimirà per tu; això no es pot desfer! + Estat del fitxer + Estat del fitxer: %s + El perfil del grup s\'emmagatzema als dispositius dels membres, no als servidors. + Per a l\'encaminament privat + xifratge correcte + Mida de la lletra + Arxius i mitjans + Els fitxers i els mitjans estan prohibits. + Interfície en francès + Ràpid i sense esperar fins que el remitent estigui en línia! + Per fi, els tenim! 🚀 + Ús de la bateria encara més reduït + Filtra els xats preferits i no llegits. + Trobar xats més ràpidament + Corregir el xifratge després de restaurar les còpies de seguretat. + Reenvia fins a 20 missatges alhora. + Finalitzar la migració a un altre dispositiu. + trucada perduda + Micròfon + Trucada perduda + Esborrany + Mòbils remots + Elimina + Expulsa + t\'ha expulsat + eliminat + Renegocia l\'encriptació + mesos + Migra aquí + Missatges enviats + Missatges rebuts + Expulsar membre + minuts + ha expulsat %1$s + moderat per %s + Error en l\'enviament del missatge + El missatge podrà ser lliurat si el membre esdevé actiu. + Missatge + El missatge és massa llarg! + missatge + MISSATGES I FITXERS + Missatges + Estat del missatge + Estat del missatge: %s + Missatge massa llarg + Menús i alertes + Eliminar la imatge + Els membres poden enviar missatges de veu. + Voleu desar les preferències? + Desa + Desa la frase de pas al Keystore + Desa la frase de pas i obre el xat + Desa i reconnecta + Repeteix + Grups més segurs + Repeteix la descàrrega + Repetir importació + Restaura la còpia de la base de dades + %s i %s + Restaura + Voleu restaurar la còpia de la base de dades? + Solament quan l\'app és oberta + Voleu revocar el fitxer? + Voleu renegociar l\'encriptació? + Desa els servidors + Missatge desat + Desa el perfil del grup + Esperant connexió mòbil: + Podeu amagar o silenciar un perfil d\'usuari; manteniu-lo premut per al menú. + Ja esteu connectant mitjançant aquest enllaç d\'un sol ús! + 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. + Podeu configurar servidors mitjançant la configuració. + Pots intentar-ho un altre cop. + 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. + has compartit enllaç d\'un sol ús en mode incògnit + Repetir la sol·licitud de connexió? + Sense informació, prova de tornar a carregar + 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. + Només dades de perfil local + Unir-se al teu grup? + El missatge es marcarà per suprimir-lo. Els destinataris podran visualitzar aquest missatge. + Nova experiència de xat 🎉 + Unir-te + Enganxar enllaç per connectar! + Arxiu de bases de dades antigues + Migrar des d\'un altre dispositiu + Les notificacions només es lliuraran fins que l\'aplicació s\'aturi! + Obrir canvis + el membre %1$s ha canviat a %2$s + Canvia l\'aspecte dels teus xats! + Màxim 40 segons, rebut a l\'instant. + Només vos podreu suprimir els missatges de manera irreversible (el vostre contacte pot marcar-los per suprimir-los). (24 hores) + Obrir configuració del servidor + altres errors + - Notificació opcional als contactes suprimits.\n- Noms de perfil amb espais.\n- I més! + Pendents + Els missatges se suprimiran; això no es pot desfer! + Connexió de xarxa més fiable. + Nou dispositiu mòbil + - El xat s\'obre amb al primer missatge no llegit.\n- Desplaçaments fins als missatges citats. + Obriu Configuració de Safari / Llocs web / Micròfon i, a continuació, trieu Permetre localhost. + Servidor de l\'operador + O enganxeu l\'enllaç de l\'arxiu + O compartiu aquest enllaç de fitxer de manera segura + enviament fallat + L\'enviament de rebuts de lliurament s\'habilitarà per a tots els contactes. + Adreça del servidor + %s pujades + Connexió TCP + Mode d\'incògnit simplificat + El rol es canviarà a %s. Tots els participants del xat rebran una notificació. + Tancar? + Les notificacions deixaran de funcionar fins que torneu a iniciar l\'aplicació + Els teus contactes romandran connectats. + Compartir l\'adreça amb els contactes? + Compartir enllaç + Desa la configuració d\'acceptació automàtica + Deixar de compartir + O per compartir en privat + Podeu crear-la més tard + Podeu fer-lo visible per als vostres contactes de SimpleX mitjançant Configuració. + Nom del perfil: + Desa i notifica el contacte + El teu perfil actual + El teu perfil s\'emmagatzema al teu dispositiu i només es comparteix amb els teus contactes, els servidors SimpleX no poden veure\'l. + Desa i notifica els contactes + Desa i notifica als membres del grup + Amagar el perfil + Contrasenya per mostrar + Desa la contrasenya del perfil + Per revelar el vostre perfil ocult introduïu una contrasenya completa al camp de cerca de la pàgina Els vostres perfils de xat. + Tu controles el teu xat! + Pots utilitzar la sintaxi markdown per donar format als teus missatges: + Com utilitzar la sintaxis markdown + trucada rebutjada + iniciant… + resposta rebuda… + esperant resposta… + esperant confirmació… + confirmació rebuda… + Un navegador web predeterminat és necessari per a les trucades. Configura\'n un al sistema i comparteix més informació amb els desenvolupadors. + Privadesa redefinida + Tu decideixes qui es pot connectar. + Immune al correu brossa + Sense identificadors d\'usuari. + Per protegir la vostra privadesa SimpleX utilitza identificadors separats per a cadascun dels vostres contactes.. + Obrir SimpleX + Com funciona SimpleX + Només els dispositius client emmagatzemen perfils d\'usuari, contactes, grups i missatges. + Notificacions privades + Com afecta la bateria + Periòdic + Instantánea + Quan l\'aplicació s\'està executant + Sense servei de fons + L\'aplicació protegeix la teva privadesa utilitzant diferents operadors en cada conversa. + Quan hi ha més d\'un operador habilitat, cap d\'ells té metadades per saber qui es comunica amb qui. + Com ajuda a la privadesa + Revisar més tard + Actualitzar + Podeu configurar els operadors a la configuració: Xarxa i Servidors. + videotrucada (sense xifrar) + Ignorar + Rebutjar + Per fer trucades, permet utilitzar el micròfon. Finalitza la trucada i prova de tornar a trucar. + Videotrucada + El servidor de retransmissió només s\'utilitza si cal. Un tercer pot observar la vostra adreça IP. + Mostrar + Trucades + sense xifratge e2e + Obrir + mitjançant retransmissor + Desactivar vídeo + p2p + Activar vídeo + Trucada rebutjada + Trucada pendent + So silenciat + Missatges omesos + El hash del missatge anterior és diferent.\" + 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. + Informeu-ho als desenvolupadors. + Pot passar quan tu o el teu contacte feu servir la còpia de seguretat de la base de dades antiga. + L\'aplicació us demanarà que confirmeu les baixades de servidors de fitxers desconeguts (excepte .onion o quan el servidor intermediari SOCKS estigui habilitat). + Envia previsualitzacions d\'enllaços + Mostra els últims missatges + Enviar + Codi nou + Desactivat + Codi d\'accés + Codi d\'accés desat! + Codi d\'accés canviat! + Nou nom mostrat: + Codi d\'autodestrucció canviat! + Codi d\'autodestrucció activat! + L\'enviament de rebuts està desactivat per a %d contactes + L\'enviament de rebuts està habilitat per a %d contactes + L\'enviament de rebuts està habilitat per a %d grups + ENVIAR ELS REBUS DE LLIURAMENT A + L\'enviament de rebuts està desactivat per a %d grups + Reiniciar + SERVIDOR INTERMEDIARI SOCKS + Imatges de perfil + TEMES + Cua + Forma del missatge + EXECUTAR SIMPLEX + Usar des d\'ordinador + Base de dades de xat + Importar base de dades + Nou arxiu de bases de dades + Obrir la carpeta de la base de dades + Aturar SimpleX? + 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. + Estableix contrasenya per a exportar + Aturar + Reinicieu l\'aplicació per utilitzar la base de dades de xat importada. + Importar + Importar la base de dades de xat? + S\'han produït alguns errors no fatals durant la importació: + Aquesta acció no es pot desfer: el vostre perfil, contactes, missatges i fitxers es perdran de manera irreversible. + Aquesta acció no es pot desfer; els missatges enviats i rebuts anteriors al seleccionat se suprimiran. Pot trigar uns quants minuts. + Podeu migrar la base de dades exportada. + Podeu desar l\'arxiu exportat. + Desa la contrasenya a la configuració + Actualitzar + Error desconegut + Contrasenya incorrecta! + Aquest grup ja no existeix. + ha desblocat a %s + %s, %s i %d membres més connectats + has canviat d\'adreça de servidor per a %s + El membre s\'eliminarà del xat; això no es pot desfer! + El membre s\'eliminarà del grup; això no es pot desfer! + El rol es canviarà a %s. Tots els membres del grup seran avisats. + La sol·licitud de connexió s\'enviarà a aquest membre del grup. + El rol es canviarà a %s. El membre rebrà una nova invitació. + Estat de la xarxa + Operador + Lloc web + Per a enviar + Restablir valors predeterminats + Compte PING + Toqueu per activar el perfil. + Silenciat quan està inactiu! + Encara rebràs trucades i notificacions de perfils silenciats quan estiguin actius. + Només el vostre contacte pot suprimir missatges de manera irreversible (podeu marcar-los per suprimir-los). (24 hores) + La supressió de missatges irreversible està prohibida en aquest xat. + Els vostres contactes poden permetre la supressió completa del missatge. + Eliminació irreversible del missatge + Amaga la pantalla de l\'aplicació a les aplicacions recents. + Millora de la privadesa i la seguretat + Configuració del servidor millorada + El(s) destinatari(s) veu(en) l\'actualització mentre l\'escriviu. + Missatges en directe + Els missatges enviats se suprimiran després del temps establert. + Esborrany de missatge + Conserva l\'últim esborrany del missatge, amb fitxers adjunts. + Aïllament de transport + Ara els administradors poden:\n- suprimir els missatges dels membres.\n- desactivar els membres (rol d\'observador) + Protegir els vostres perfils de xat amb contrasenya! + Admet bluetooth i altres millores. + Mitjançant protocol segur de resistència quàntica. + Enllaça aplicacions mòbils i d\'ordinador! 🔗 + Migrar a un altre dispositiu + Preparant càrrega + Sense connexió de xarxa + Sessions de transport + Assegurades + Usar l\'aplicació durant la trucada. + Desblocar per tothom + Els missatges es marcaran com a moderats per a tots els membres. + Utilitzar %s + Suprimir la contrasenya de Keystore? + Suprimir la contrasenya de la configuració? + Nova contrasenya… + Introduïu la contrasenya actual correcta. + Estableix contrasenya per a la base de dades + Estableix contrasenya + Actualitzar la contrasenya de la base de dades + La frase de contrasenya s\'emmagatzema a la configuració com a text pla. + L\'intent de canviar la contrasenya de la base de dades no s\'ha completat. + Error de base de dades desconegut: %s + Error en restaurar base de dades + Migracions: %s + Avís: podeu perdre algunes dades! + Podeu iniciar el xat mitjançant Configuració / Base de dades o reiniciant l\'aplicació. + Uneix-te d\'incògnit + Entrant al grup + Estàs convidat al grup. Uneix-te per connectar amb els seus membres. + T\'has unit a aquest grup. S\'està connectant amb l\'emissor de la invitació. + Deixaràs de rebre missatges d\'aquest xat. L\'historial de xat es conservarà. + Deixaràs de rebre missatges d\'aquest grup. L\'historial de xat es conservarà. + %s i %s connectats + %s, %s i %d membres + Obrir + %s, %s i %s connectats + adreça de contacte eliminat + foto de perfil eliminada + establir una nova foto de perfil + nova adreça de contacte + perfil actualitzat + has canviat d\'adreça de servidor + propietari + estat desconegut + Podeu compartir aquesta adreça amb els vostres contactes perquè es connectin amb %s. + Aquest grup té més de %1$d membres, no s\'envien rebuts d\'entrega. + Rebut + %s a les %s + Enviar missatge directe + Moderat el: %s + Rebut: %s + Desblocar membre + Desblocar membre? + Desblocar membre per tothom? + Rol + informació de cua del servidor: %1$s\n\núltim missatge rebut: %2$s + Informació de la cua de missatges + cap + Enviant via + El teu perfil de xat s\'enviarà als membres del xat + El teu perfil de xat s\'enviarà als membres del grup + Operador de xarxa + %s servidors + Servidors per a fitxers nous al vostre perfil de xat actual + Veure condicions + Obrir condicions + Servidor afegit a l\'operador %s. + Ha canviat l\'operador del servidor. + El protocol del servidor ha canviat. + Temps d\'espera de connexió TCP + Temps d\'espera del protocol + Temps d\'espera del protocol per KB + Concurrència a la recepció + Actualitzar la configuració de xarxa? + Actualitzar + Connexions de perfil i servidor + Mostrar + Silenciar + Activar so + Mostrar perfil + Restablir color + Restablir colors + Mode de sistema + no + desactivat` + Activat + Restablir al tema de l\'aplicació + Restablir al tema d\'usuari + Estableix tema predeterminat + Permeteu + Estableix preferències de grup + Preferències + Reaccions a missatge + Missatges de veu + Prohibir l\'enviament de fitxers i mitjans. + Enllaços SimpleX no permesos. + S\'envien fins a 100 darrers missatges als nous membres. + L\'historial no s\'envia als nous membres. + oferit %s + oferit %s: %2s + Novetats + Nou a %s + Llegeix més + Missatges de veu + Amb missatge de benvinguda opcional. + Noms de fitxers privats + Ús de bateria reduït + Gràcies als usuaris: contribuïu a través de Weblate! + Aviat hi haurà més millores! + Rebuts de lliurament de missatges! + Activa o desactiva l\'incògnit en connectar-te. + Per amagar missatges no desitjats. + Notes privades + La barra de cerca accepta enllaços d\'invitació. + Amb fitxers i mitjans xifrats. + Entrega de missatges millorada + Amb ús reduït de la bateria. + Migra a un altre dispositiu mitjançant el codi QR. + Trucades imatge-en-imatge + Xifratge resistent quàntic + Formar imatges de perfil + L\'autoria del missatge continua sent privada. + Quadrat, cercle o qualsevol forma intermèdia. + En connectar trucades d\'àudio i vídeo. + S\'habilitarà als xats directes! + Gestió de la xarxa + Nous temes de xat + Rebre fitxers amb seguretat + Entrega de missatges millorada + IU en persa + Amb ús reduït de la bateria. + Noves opcions de mitjans + Barres d\'eines de xat accessible + Fes servir l\'aplicació amb una sola mà. + Reprodueix des de la llista de xat. + Actualitza l\'aplicació automàticament + Descentralització de la xarxa + El segon operador preestablert a l\'aplicació! + Privadesa per als teus clients. + Veure condicions actualitzades + hores + setmanes + L\'enviament de rebuts de lliurament s\'habilitarà per a tots els contactes de tots els perfils de xat visibles. + Desenllaçar + Desenllaçar l\'ordinador? + Podeu activar-los més tard mitjançant la configuració de privadesa i seguretat de l\'aplicació. + Aquest enllaç s\'ha utilitzat amb un altre dispositiu mòbil; creeu-ne un de nou a l\'ordinador. + Només un dispositiu pot funcionar al mateix temps + Codi de sessió + Esperant ordinador… + Ordinadors enllaçats + Verifica el codi a l\'ordinador + S\'ha arribat al temps d\'espera durant la connexió a l\'ordinador + Ja heu sol·licitat connexió a través d\'aquesta adreça! + Repetir la sol·licitud d\'unió? + Grup obert + Ja t\'estàs unint al grup mitjançant aquest enllaç. + Informeu-ho als desenvolupadors:\n%s\n\nEs recomana reiniciar l\'aplicació. + O importar un fitxer d\'arxiu + Enganxa l\'enllaç de l\'arxiu + Reiniciar xat + Migrant + Preparant descàrrega + Importació fallida + Important arxiu + Aturant el xat + Migrar dispositiu + Per continuar, el xat s\'ha d\'aturar. + Carregant l\'arxiu + Suprimir l\'arxiu? + Repetir la càrrega + Pots intentar-ho un altre cop. + L\'arxiu de base de dades carregat s\'eliminarà permanentment dels servidors. + Migració completada + Verificar contrasenya + Altre + Info servidors + Mostrant info per a + Iniciat el %s\nTotes les dades es mantenen privades al vostre dispositiu. + Total + No esteu connectats a aquests servidors. Per enviar missatges s\'usa l\'encaminament privat. + Reconnectar servidors? + Restablir + Restablir totes les estadístiques + Restablir totes les estadístiques? + Les estadístiques dels servidors es restabliran; això no es pot desfer! + Carregat + Missatges enviats + Missatges rebuts + Total rebuts + Total enviats + Iniciat el %s. + Errors de recepció + Reconnectar + Errors d\'enviament + altres + Intermediat + Enviat directament + Enviat mitjançant servidor intermediari + Subscrit + Errors de subscripció + Subscripcions ignorades + Fitxers carregats + Errors de càrrega + Utilitzar servidors + Rebuts desactivats + Reconnectar els servidors per forçar l\'entrega de missatges. Utilitza trànsit addicional. + Reconnectar servidor? + Reconnectar tots els servidors + 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. + 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. + Interval PING + Informeu-ho als desenvolupadors:\n%s + Encaminament de missatges privat 🚀 + Amagar + Reconnectar el servidor per forçar l\'entrega de missatges. Utilitza trànsit addicional. + Verificar contrasenya de la base de dades + Navegació millorada al xat + Recepció de missatges + Enllaç no vàlid + Unir-te al grup? + marcat eliminat + Obrint la base de dades… + Comproveu que l\'enllaç SimpleX sigui correcte. + l\'enviament de fitxers encara no està suportat + Intentant connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte. + S\'està provant de connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte (error: %1$s). + Usar perfil actual + Usar nou perfil incògnit + Error aplicació + Esteu connectat al servidor utilitzat per rebre missatges d\'aquest contacte. + El teu perfil s\'enviarà al contacte del qual has rebut aquest enllaç. + Heu compartit una ruta de fitxer no vàlida. Informeu-ne als desenvolupadors de l\'aplicació. + Us connectareu amb tots els membres del grup. + d\'incògnit mitjançant l\'enllaç de l\'adreça de contacte + d\'incògnit mitjançant l\'enllaç del grup + d\'incògnit mitjançant un enllaç d\'un sol ús + xat no vàlid + format de missatge no vàlid + convidat a connectar + 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. + Notes privades + la recepció de fitxers encara no està suportada + sol·licitada connexió + desat des de %s + Adreça de contacte SimpleX + Enllaç de grup SimpleX + Enllaços SimpleX + Aquest xat està protegit per xifratge d\'extrem a extrem. + Aquest xat està protegit per un xifratge d\'extrem a extrem resistent a la quàntica. + format de missatge desconegut + mitjançant %1$s + Mitjançant navegador + mitjançant enllaç d\'adreça de contacte + mitjançant enllaç de grup + mitjançant enllaç d\'un sol ús + has compartit un enllaç d\'un sol ús + Nom mostrat no vàlid! + Assegureu-vos que les adreces del servidor SMP estiguin en el format correcte, que estiguin separades per línies i que no estiguin duplicades. + Assegureu-vos que les adreces del servidor XFTP estiguin en el format correcte, que estiguin separades per línies i que no estiguin duplicades. + No hi ha servidors multimèdia ni de fitxers. + No hi ha servidors de missatges. + No hi ha servidors per a l\'encaminament de missatges privats. + No hi ha servidors per rebre fitxers. + No hi ha servidors per rebre missatges. + No hi ha servidors per enviar fitxers. + Comproveu la vostra connexió de xarxa amb %1$s i torneu-ho a provar. + Si us plau, prova-ho més tard. + Actualitzeu l\'aplicació i contacteu amb els desenvolupadors. + Error d\'encaminament privat + L\'adreça del servidor és incompatible amb la configuració de xarxa: %1$s. + La versió del servidor és incompatible amb la vostra aplicació: %1$s. + Aquest nom mostrat no és vàlid. Si us plau, trieu-ne un altre. + Ja teniu un perfil de xat amb el mateix nom mostrat Si us plau, trieu un altre nom. + Enllaç de connexió no vàlid + El remitent ha cancel·lat la transferència de fitxers. + Servidors desconeguts! + Ja esteu connectat a %1$s. + Comproveu que heu utilitzat l\'enllaç correcte o demaneu al vostre contacte que us n\'enviï un altre. + És possible que el remitent hagi suprimit la sol·licitud de connexió. + La connexió ha arribat al límit de missatges no lliurats, és possible que el vostre contacte estigui fora de línia. + Missatges no lliurats + 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. + Possiblement, l\'empremta digital del certificat a l\'adreça del servidor és incorrecta + El servidor requereix autorització per crear cues, comproveu la contrasenya + El servidor requereix autorització per carregar, comproveu la contrasenya + La prova ha fallat al pas %s. + Cua segura + Carrega fitxer + Notificacions instantànies + Notificacions instantànies! + Les notificacions instantànies estan desactivades! + Notificacions periòdiques + Notificacions periòdiques desactivades + 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. + Obriu la configuració de l\'aplicació + Sense trucades en segon pla + És possible que l\'aplicació es tanqui al cap d\'un minut en segon pla. + Cal contrasenya + Per rebre notificacions introduïu la contrasenya de la base de dades + Rebent missatges… + Servei de SimpleX Xat + La base de dades no funciona correctament. Toca per obtenir més informació + Videotrucada + Servei de notificacions + Vista prèvia de la notificació + Vista prèvia + Cíclic + Amaga el contacte i el missatge + Text del missatge + Nova sol·licitud de contacte + missatge nou + Mostrar el contacte i el missatge + Mostrar només el contacte + Sense codi d\'accés de l\'aplicació + Camp de codi d\'accés + Bloqueig SimpleX + Mode de Bloqueig SimpleX + Autenticació del sistema + Per protegir la vostra informació activeu SimpleX Lock.\nSe us demanarà que completeu l\'autenticació abans que aquesta funció estigui habilitada. + Activar + No has pogut ser verificat; si us plau, torna-ho a provar. + Recordeu-la o emmagatzemeu-la de manera segura: no hi ha manera de recuperar una contrasenya perduda! + Bloqueig SimpleX habilitat + Se us demanarà que us autentiqueu quan inicieu o reinicieu l\'aplicació després de 30 segons en segon pla. + Inicieu sessió amb la vostra credencial + Avís d\'entrega de missatge + El més probable és que aquest contacte hagi suprimit la connexió amb tu. + Cap missatge + Obrir consola de xat + Obrir pantalla de migració + Bloqueig SimpleX no habilitat! + Aturar SimpleX + Podeu activar el Bloqueig SimpleX mitjançant Configuració. + Aquest missatge s\'ha suprimit o encara no s\'ha rebut. + Problemes de xarxa: el missatge ha caducat després de molts intents d\'enviar-lo. + Clau incorrecta o connexió desconeguda: el més probable és que aquesta connexió s\'hagi suprimit. + L\'adreça del servidor és incompatible amb la configuració de la xarxa. + La versió del servidor és incompatible amb la configuració de la xarxa. + Clau incorrecta o adreça de bloc de fitxer desconeguda: el més probable és que el fitxer s\'hagi suprimit. + Missatge rebut + Missatge enviat + Sense historial + Desat des de + El(s) destinatari(s) no veu(en) de qui és aquest missatge. + Sense informació de lliurament + El missatge se suprimirà; això no es pot desfer! + Els missatges es marcaran per eliminar-los. Els destinataris podran revelar aquests missatges. + Els missatges s\'eliminaran per a tots els membres. + El missatge s\'eliminarà per a tots els membres. + El missatge es marcarà com a moderat per a tots els membres. + S\'aturarà la recepció del fitxer. + L\'enviament del fitxer s\'aturarà. + Aturar fitxer + Deixar de rebre fitxer? + Deixar d\'enviar el fitxer? + Missatge reenviat + Encara no hi ha connexió directa, el missatge el reenvia l\'administrador. + Revocar fitxer + enviament no autoritzat + Aquest text està disponible a la configuració + Benvingut %1$s! + unir-te com a %s + enviar missatge directe + Toca per iniciar un xat nou + Carregant xats… + No hi ha xats filtrats + Cerqueu o enganxeu l\'enllaç SimpleX + Toqueu per connectar + No tens cap xat + No hi ha cap xat seleccionat + Res seleccionat + Seleccionats %d + Res a reenviar! + Els missatges s\'han suprimit després de seleccionar-los. + Compartir fitxer… + Compartir mitjans… + Compartir missatge… + Les preferències de xat seleccionades prohibeixen aquest missatge. + Massa imatges! + Massa vídeos! + Només es poden enviar 10 imatges al mateix temps + Només es poden enviar 10 vídeos al mateix temps + La imatge no es pot descodificar. Si us plau, proveu amb una imatge diferent o contacteu amb els desenvolupadors. + El vídeo no es pot descodificar. Si us plau, prova amb un vídeo diferent o contacta amb els desenvolupadors. + ets observador + No pots enviar missatges! + Poseu-vos en contacte amb l\'administrador del grup. + Només els propietaris del grup poden activar fitxers i mitjans. + Desant %1$s missatges + Envia missatge directe per connectar + Enllaços SimpleX no permesos. + Si us plau, redueix la mida del missatge i torna a enviar-lo. + Si us plau, reduïu la mida del missatge o elimineu mitjans i torneu a enviar-lo. + Missatges de veu no permesos + Podeu copiar i reduir la mida del missatge per enviar-lo. + Imatge + Imatge enviada + Esperant la imatge + Esperant la imatge + Imatge desada a la Galeria + La imatge es rebrà quan el vostre contacte acabi de carregar-la. + La imatge es rebrà quan el vostre contacte estigui en línia, espereu o comproveu més tard! + Esperant el vídeo + Arxiu gran! + Vídeo enviat + El vídeo es rebrà quan el contacte acabi de pujar-lo. + El vídeo es rebrà quan el contacte estigui en línia, espereu o comproveu més tard! + Esperant el vídeo + El vostre contacte ha enviat un fitxer més gran que la mida màxima admesa actualment (%1$s). + Carregant el fitxer + Notificacions + obrir + Si us plau, espereu mentre es carrega el fitxer des del mòbil enllaçat + Error de fitxer temporal + Missatge de veu + Missatge de veu… + Missatge de veu (%1$s ) + Esperant el fitxer + Només suprimeix la conversa + Pendente + 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. + Estableix el nom del contacte… + Pots enviar missatges a %1$s des dels contactes arxivats. + Encara podeu veure la conversa amb %1$s a la llista de xats. + El xifratge funciona i el nou acord de xifratge no és necessari. Pot resultar en errors de connexió! + Renegociar + Veure codi de seguretat + Comprovar codi de seguretat + Enviar Missatge + Grava el missatge de veu + Heu de permetre que el vostre contacte enviï missatges de veu per poder-los enviar. + Missatge en directe! + cap detall + OK + Enllaç d\'invitació d\'un sol ús + Només els propietaris de grups poden activar els missatges de veu. + (només emmagatzemat pels membres del grup) + Permís denegat! + Demaneu al vostre contacte que habiliti l\'enviament de missatges de veu. + Envia un missatge en directe: s\'actualitzarà per al(s) destinatari(s) a mesura que l\'escrius + Envia un missatge que desapareix + Envia missatge en directe + Toqueu per escanejar + (per compartir amb el teu contacte) + Missatges de veu prohibits! + Gràcies per instal·lar SimpleX Xat! + Per iniciar un xat nou + Toqueu el botó + Si decideixes rebutjar, el remitent NO rebrà notificació. + Si heu rebut l\'enllaç d\'invitació de SimpleX Chat, podeu obrir-lo al vostre navegador: + Rebutjar + Per connectar-se mitjançant enllaç + vista prèvia de l\'enllaç + Marcar com a llegit + imatge de perfil + espai per a la imatge de perfil + Codi QR + Estableix el nom del contacte + La connexió que heu acceptat es cancel·larà! + El contacte amb qui has compartit aquest enllaç NO es podrà connectar! + Desfavorit + vol contactar amb tu! + Has convidat un contacte + 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). + Mostrar codi QR + Aquest no és un enllaç de connexió vàlid! + Aquest codi QR no és un enllaç! + Et connectaràs al grup quan el dispositiu de l\'amfitrió estigui en línia. Espereu o comproveu més tard! + Et connectaràs quan s\'accepti la teva sol·licitud de connexió, si us plau, espera o consulta més tard! + Si no pots trobar-te en persona, mostra el codi QR en una videotrucada o comparteix l\'enllaç. + Enganxeu l\'enllaç que heu rebut per connectar amb el vostre contacte… + Compartir enllaç d\'un sol ús + Compartir enllaç d\'un sol ús amb un amic + Compartir adreça públicament + Comparteix l\'adreça SimpleX a les xarxes socials. + Per connectar-se, el vostre contacte pot escanejar el codi QR o utilitzar l\'enllaç de l\'aplicació. + Per protegir-vos de la substitució del vostre enllaç, podeu comparar els codis de seguretat de contacte. + Quan algú sol·liciti la connexió, pots acceptar-la o rebutjar-la. + Podeu definir el nom de la connexió per recordar amb qui s\'ha compartit l\'enllaç. + Pots compartir la teva adreça com a enllaç o codi QR; qualsevol es pot connectar amb tu. + S\'enviarà el teu perfil de xat\nal teu contacte + El teu perfil %1$s es compartirà. + Et connectaràs quan el dispositiu del teu contacte estigui en línia, si us plau, espera o consulta més tard! + Si més tard decideixes eliminar la teva adreça els contactes no es perdran. + Mantenir la invitació no utilitzada? + Nou xat + Missatge nou + No hi ha contactes filtrats + Enllaç d\'invitació d\'un sol ús + O escaneja el codi QR + O mostra aquest codi + Enganxar enllaç + Enganxar l\'enllaç rebut + Seleccioneu el perfil de xat + Compartir perfil + Compartir aquest enllaç d\'un sol ús + Toca per enganxar l\'enllaç + El codi QR que heu escanejat no és un enllaç de SimpleX. + El text enganxat no és un enllaç SimpleX. + Aquesta cadena no és un enllaç de connexió! + Podeu tornar a veure l\'enllaç d\'invitació als detalls de connexió. + La teva connexió s\'ha mogut a %s, però s\'ha produït un error inesperat en redirigir-te al perfil. + Marcar com a verificat + Escaneja el codi de seguretat des de l\'aplicació del teu contacte. + Contactes + Com utilitzar-lo + Servidors de fitxers i mitjans + Servidors de missatges + Nou servidor + Altres servidors SMP + Altres servidors XFTP + Servidor preestablert + Adreça predeterminada del servidor + Preguntes i idees + Contacta via email + La prova del servidor ha fallat! + Bloqueig SimpleX + %s no està verificat + %s està verificat + Alguns servidors han fallat la prova: + Provar servidor + Provar servidors + Servidors per a noves connexions del vostre perfil de xat actual + Utilitzar per a noves connexions + Utilitzar servidor + Perfils de xat + El teu servidor + L\'adreça del teu servidor + Configuració + La teva adreça SimpleX + Com + Com utilitzar els vostres servidors + Servidors ICE (un per línia) + Instal·lar SimpleX Chat per al terminal + 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. + Valoreu l\'aplicació + Restablir totes les pistes + Els servidors WebRTC ICE desats s\'eliminaran. + Mostrar percentatge + Estrela a GitHub + Utilitzar servidors SimpleX Xat? + Usant servidors SimpleX Xat. + Servidors ICE + Servidors SMP + Servidors XFTP + Xarxa i servidors + Autenticació d\'intermediari + Servidor intermediari SOCKS + Configuració SOCKS + Utilitzar credencials aleatòries + Utilitzar servidor intermediari SOCKS + Si confirmeu, els servidors de missatgeria podran veure la vostra adreça IP i el vostre proveïdor, a quins servidors us esteu connectant. + Les noves credencials de SOCKS s\'utilitzaran cada vegada que inicieu l\'aplicació. + S\'utilitzaran noves credencials SOCKS per a cada servidor. + Es necessitaran hosts .onion per a la connexió.\nTingueu en compte: no us podreu connectar als servidors sense l\'adreça .onion. + S\'utilitzaran amfitrions .onion quan estiguin disponibles. + No es faran servir hosts .onion + port %d + Encaminament privat + Servidor + Aïllament de transport + Actualitzar el mode d\'aïllament de transport? + Utilitza credencials de servidors intermediari diferents per a cada connexió. + Utilitza credencials de servidor intermediari diferents per a cada perfil. + Utilitzar connexió a Internet directa? + Utilitzar servidors .onion + Utilitzar servidor intermediari SOCKS? + Si disponibles + Les vostres credencials es podrien enviar sense xifrar. + COLORS DE LA INTERFÍCIE + Alternativa d\'encaminament de missatges + Mode d\'encaminament de missatges + Obrir ubicació del fitxer + Envieu missatges directament quan l\'adreça IP està protegida i el vostre servidor de destinació no admet l\'encaminament privat. + Enviar missatges directament quan el vostre servidor de destinació no admet l\'encaminament privat. + Mostrar l\'estat del missatge + simplexmq: v%s (%2s) + Ometre aquesta versió + Per protegir la vostra adreça IP l\'encaminament privat utilitza els vostres servidors SMP per enviar missatges. + Servidors desconeguts + Actualització disponible: %s + Utilitzar l\'encaminament privat amb servidors desconeguts. + Utilitzeu l\'encaminament privat amb servidors desconeguts quan l\'adreça IP no estigui protegida. + Amb IP oculta + Descàrrega d\'actualització cancel·lada + Si us plau, reinicieu l\'aplicació. + Recorda més tard + Mostrar opcions de desenvolupador + Mostrar errors interns + Mostrar trucades lentes d\'API + Per rebre notificacions sobre les noves versions activeu la comprovació periòdica de les versions Estable o Beta. + Compartir amb contactes + La plataforma de missatgeria i aplicacions que protegeix la vostra privadesa i seguretat. + El perfil només es comparteix amb els teus contactes. + No emmagatzemem cap dels vostres contactes o missatges (un cop lliurats) als servidors. + El vostre perfil, contactes i missatges lliurats s\'emmagatzemen al vostre dispositiu. + Obrir configuració + El futur de la missatgeria + Operadors de xarxa + Notificacions i bateria + La contrasenya aleatòria s\'emmagatzema a la configuració com a text pla.\nPodeu canviar-ho més tard. + Seleccioneu els operadors de xarxa que voleu utilitzar. + Operadors de servidor + Configura la contrasenya de la base de dades + Utilitzar contrasenya aleatòria + Obrir SimpleX Chat per acceptar la trucada + El servidor de retransmissió protegeix la vostra adreça IP, però pot veure la durada de la trucada. + Servidors WebRTC ICE + Servidors ICE + Privacitat i seguretat + Protegeix la pantalla de l\'aplicació + Protegir l\'adreça IP + Privacitat + Si introduïu aquesta contrasenya en obrir l\'aplicació, totes les dades de l\'aplicació s\'eliminaran de manera irreversible. + Si introduïu el vostre codi d\'autodestrucció mentre obriu l\'aplicació: + Estableix codi + Aquesta configuració és per al vostre perfil actual + Es pot canviar a la configuració de contacte i grup. + No + CONFIGURACIÓ + Tou + Fort + SUPORT SIMPLEX XAT + Connexió a la xarxa + ENCAMINAMENT DE MISSATGES PRIVAT + mai + No s\'han rebut ni enviats fitxers + Reinicieu l\'aplicació per crear un perfil de xat nou. + Aquesta acció no es pot desfer: se suprimiran tots els fitxers i els mitjans rebuts i enviats. Les imatges de baixa resolució es mantindran. + 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. + Aquesta configuració s\'aplica als missatges del vostre perfil de xat actual + La vostra base de dades de xat no està xifrada; definiu una contrasenya per protegir-la. + La frase de contrasenya s\'emmagatzemarà a la configuració com a text pla després de canviar-la o reiniciar l\'aplicació. + Contrasenya de la base de dades incorrecta + Heu d\'introduir la contrasenya cada vegada que s\'inicia l\'aplicació: no s\'emmagatzema al dispositiu. + Error en Keystore + Obrir xat + Si us plau, emmagatzemeu la contrasenya de manera segura, NO podreu accedir al xat si la perdeu. + Si us plau, emmagatzemeu la contrasenya de manera segura, NO la podreu canviar si la perdeu. + Confirmació de migració no vàlida + 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. + Barres d\'eines d\'aplicacions accessible + Barres d\'eines de xat accessible + Mostra la llista de xat en una finestra nova + Commuta la llista de xat: + Mostrar consola en finestra nova + Podeu canviar-la a la configuració de l\'aparença. + Actualitzar i obrir el xat + Toca per unir-te + Toca per unir-te d\'incògnit + T\'has unit a aquest grup + Has rebutjat la invitació del grup + Esteu utilitzant un perfil d\'incògnit en aquest grup; per evitar mostrar el vostre perfil principal no es permet convidar contactes + Has enviat la invitació del grup + convidat mitjançant l\'enllaç del vostre grup + ha actualitzat el perfil del grup + has canviat el rol de %s a %s + has blocat a %s + has canviat el teu rol a %s + has marxat + has foragitat a %1$s + has desblocat a %s + observador + xifratge extrem a extrem resistent quàntic + Rol inicial + Convidar a xatejar + Nou rol de membre + No hi ha contactes per afegir + Membre anterior %1$s + Ometre convidar membres + desconegut + Convidar membres + Cap contacte seleccionat + 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 + Missatge de benvinguda + Només els propietaris del xat poden canviar les preferències. + Només els propietaris del grup poden canviar-ne les preferències. + Compartir adreça + Enviar rebuts + Enviat + Registre actualitzat + Moderat el + Registre actualitzat: %s + Enviat: %s + sense text + Eliminar membre? + %s: %s + Eliminar membre + Els missatges de %s es mostraran! + Demaneu al vostre contacte que habiliti les trucades. + Enviar un missatge per activar trucades. + Heu de permetre que el vostre contacte truqui per poder trucar-los. + Vista prèvia + Rebent via + Desa i actualitza el perfil del grup + SERVIDORS + Missatge de benvinguda + El missatge de benvinguda és massa llarg + Els teus servidors + Servidors preestablerts + Revisar condicions + Per a rebre + Utilitzar per a fitxers + Utilitzar per a missatges + Fes el perfil privat! + Permet tenir moltes connexions anònimes sense cap dada compartida entre elles en un únic perfil de xat. + El teu perfil aleatori + Mostrar perfil ocult + Quan comparteixes un perfil d\'incògnit amb algú, aquest perfil s\'utilitzarà per als grups als quals et conviden. + Tema + Importar tema + Error en importar tema + Assegureu-vos que el fitxer tingui la sintaxi YAML correcta. Exporta el tema per tenir un exemple de l\'estructura del fitxer del tema. + Missatge enviat + Resposta enviada + Títol + Missatge rebut + Resposta rebuda + Color imatge de fons + Color de fons + Transparència + Zoom + Enllaços SimpleX + Historial visible + desactivat + recepció no permesa + Establir 1 dia + Només tu pots enviar missatges que desapareixen. + Només el vostre contacte pot enviar missatges que desapareixen. + Prohibir l\'enviament de missatges de veu. + Només tu pots enviar missatges de veu. + Només el vostre contacte pot enviar missatges de veu. + Els missatges de veu estan prohibits en aquest xat. + Només tu pots afegir reaccions als missatges. + Només el vostre contacte pot afegir reaccions als missatges. + Les reaccions als missatges estan prohibides en aquest xat. + Només tu pots fer trucades. + Només el vostre contacte pot fer trucades. + Prohibir l\'enviament de missatges de veu. + Prohibir l\'enviament d\'enllaços SimpleX + Es prohibeix la supressió irreversible de missatges. + Els membres poden afegir reaccions als missatges. + Els membres poden suprimir de manera irreversible els missatges enviats. (24 hores) + Els membres poden enviar missatges que desapareixen. + Els membres poden enviar enllaços SimpleX. + Les reaccions als missatges estan prohibides. + Envia fins a 100 darrers missatges als nous membres. + Els missatges de veu estan prohibits. + propietaris + Múltiples perfils de xat + Gràcies als usuaris: contribuïu a través de Weblate! + Comprovar la seguretat de la connexió + Estableix el missatge que es mostra als nous membres! + Interfície d\'usuari en japonès i portuguès + Reaccions a missatges + Aviat hi haurà més millores! + Interfície en polonès + Codi d\'autodestrucció + Configureu-lo en lloc de l\'autenticació del sistema. + Gràcies als usuaris: contribuïu a través de Weblate! + Gràcies als usuaris: contribuïu a través de Weblate! + Gràcies als usuaris: contribuïu a través de Weblate! + Vídeos i fitxers de fins a 1 GB + - Missatges de veu fins a 5 minuts.\n- Temps personalitzat per a missatges temporals.\n- Historial d\'edició. + - Lliurament de missatges més estable.\n- Grups millorats.\n- I més! + Nova aplicació per a ordinador! + La doble comprovació que ens mancava! ✅ + Manté les vostres connexions + Fer desaparèixer un missatge + Unir-se a converses de grup + Historial recent i bot de directori millorat. + Canvia l\'àudio i el vídeo durant la trucada. + Canvia el perfil de xat per a invitacions d\'un sol ús. + Podeu activar-ho més tard mitjançant Configuració + Enllaçar un mòbil + Mòbils enllaçats + Nom d\'aquest dispositiu + Verificar codi al mòbil + Verificar connexió + El nom del dispositiu es compartirà amb el client mòbil connectat. + Aquest dispositiu + Opcions ordinador enllaçat + Cap mòbil connectat + No compatible! + Obrir port al tallafoc + Enganxar adreça d\'ordinador + Aleatori + Recarregar + Escaneja el codi QR des de l\'ordinador + Per permetre que una aplicació mòbil es connecti a l\'ordinador obriu aquest port al vostre tallafoc, si el teniu habilitat + Verificar connexions + Aquesta funció encara no està disponible. Prova la propera versió. + Aquest és el vostre enllaç d\'un sol ús! + Aquesta és la teva pròpia adreça SimpleX! + Confirmeu que la configuració de xarxa és correcta per a aquest dispositiu. + Servidors connectats prèviament + Servidors intermediats + Com funciona + IU en hongarès i turc + Codi d\'accés no canviat! + Protegiu la vostra adreça IP dels servidors de retransmissió de missatgeria escollits pels vostres contactes.\nActiva a la configuració de *Xarxa i servidors*. + Toqueu Crea adreça SimpleX al menú per crear-la més tard. + Per protegir la zona horària els fitxers d\'imatge/veu utilitzen UTC. + Desbloca + La càrrega ha fallat + Per verificar el xifratge d\'extrem a extrem amb el vostre contacte compareu (o escanegeu) el codi dels vostres dispositius. + L\'actualització de la configuració reconnectarà el client a tots els servidors. + Atenció: l\'inici del xat a diversos dispositius és incompatible i provocaria errors en el lliurament de missatges. + Canvia + Voleu desar les preferències? + Voleu deixar de compartir l\'adreça? + secret + Altaveu desactivat + Altaveu activat + Sense Tor o una VPN, el servidors de fitxers podran veure la vostra adreça. + Sistema + Autodestrucció + Codi d\'autodestrucció + Grups petits (màx. 20) + Alguns fitxers no han estat exportats + s + Mida + Voleu iniciar un xat? + Escala + + Selecciona + %s baixades + WiFi + Ethernet per cable + Estadistiques + Servidor XFTP + Avaluació de seguretat + Servidor SMP + segons + Inicia un xat + "Invitació d\'un sol ús per SimpleX" + Sense Tor o una VPN, la vostra adreça serà visible per als següents relays XFTP: %1$s. + Funció lenta + heu estat convidat a un grup + cerca + (escaneja o enganxa del porta-retalls) + Escaneja un codi QR + Comença una conversa nova + Logo de SimpleX + Equip SimpleX + Heu acceptat la connexió + Escaneja / Enganxa un enllaç + Escaneja codi + Codi de seguretat + Voleu desar els servidors? + Escaneja el codi QR del servidor + Servidors SMP + Servidors XFTP + Altaveu + VÓS + %s segon(s) + "Heu estat convidat a un grup" + %s connectat + codi de seguretat modificat + encriptació extrem a extrem estàndard + Selecciona contactes + vós : %1$s + %s (actual) + Voleu desar el missatge de benvinguda? + Sistema + Sistema + Sistema + Secundari + Protocols de SimpleX revisats per Trail of Bits. + Escaneja amb el mòbil + Vídeo + vídeo + Tema del perfil + Contrasenya del perfil + No es permeten trucades ni videotrucades. + No es permeten reaccions als missatges. + No es permet l\'enviament de missatges temporals. + No es permet l\'eliminació irreversible de missatges. + No es permeten reaccions als missatges. + No es permet l\'enviament de missatges directes als membres. + No es permet l\'enviament de missatges temporals. + L\'actualització del perfil s\'enviarà als vostres contactes. + a + b + ratllat + Suprimeix els missatges després + Eliminar el perfil de xat per + La connexió no està preparada. + Error en crear la llista de xat + Error en carregar llistes de xat + Error en actualitzar les llistes de xat + Contactes + Preferits + Cap xat no llegit + Afegir llista + Tot + Negocis + Obrir amb %s + Sense xats + No s\'han trobat xats + Canviar llista + Eliminar + Grups + No hi ha cap xat a la llista %s. + Llista + El nom de la llista i l\'emoji haurien de ser diferents per a totes les llistes. + Notes + Afegir a la llista + Crear llista + Desar llista + Tots els xats s\'eliminaran de la llista %s i aquesta serà suprimida + Canviar ordre + Editar + Eliminar llista? + Nom de la llista... + Spam + Arxivar + Arxivar informe + Informar de spam: només ho veurà la moderació del grup. + Informar del perfil d\'un/a membre: només ho veurà la moderació del grup. + Informar de violació: només ho veurà la moderació del grup. + Informar de contingut: només ho veurà la moderació del grup. + Informar d\'altres: només ho veurà la moderació del grup. + informe arxivat per %s + Un altre motiu + Violació de les normes de la comunitat + Contingut inadequat + Perfil inadequat + Suprimeix l\'informe + L\'informe s\'arxivarà. + Informar + Spam + El contingut infringeix les condicions d\'ús + Connexió blocada + La connexió està bloquejada per l\'operador del servidor:\n%1$s. + Arxivar informe? + El fitxer està blocat per l\'operador del servidor:\n%1$s. + Motiu de l\'informe? + 1 informe + %d informes + Informes de membres + Informes + Error en desar configuració + No + Obrir l\'enllaç web? + + Obrir enllaç + moderador + Preguntar + Obrir enllaços de la llista de xat + informes arxivats + Només ho veuen remitents i moderació + Només ho veieu vosaltres i moderació + Error en crear informe + Establir nom del xat… + Suprimiu els missatges de xat del vostre dispositiu. + Voleu canviar la supressió automàtica de missatges? + predeterminat (%s) + 1 any + Desactivar la supressió de missatges + Desactivar la supressió automàtica de missatges? + Aquesta acció no es pot desfer; els missatges enviats i rebuts en aquest xat anteriors al seleccionat se suprimiran. + Els missatges d\'aquest xat no se suprimiran mai. + Port TCP per a missatgeria + Emprar port web + Emprar el port TCP %1$s quan no se n\'especifica cap. + Silenciar tot + Mencions no llegides + Podeu mencionar fins a %1$s membres per missatge! + Permetre denunciar missatges a moderació. + No permetre denunciar missatges a moderació. + Arxivar totes les denúncies? + Arxivar %d denúncies? + Arxivar denúncies + Per a totes les moderadores + Per a mi + Denúncia: %s + En aquest grup no es permet denunciar missatges. + Els/les membres poden denunciar els missatges a moderació. + Totes les denúncies s\'arxivaran. + Mencionar els membres 👋 + Enviar denúncies privades + Millor privadesa i seguretat + No us perdeu missatges importants. + Enviament de missatges més ràpid. + Organitzar els xats en llistes + Noms de fitxers multimèdia privats. + Establir la caducitat del missatge als xats. + Rebeu notificació quan se us menciona. + Millor rendiment dels grups + Supressió més ràpida de grups. + Ajudar els administradors a moderar els seus grups. + rebutjat + rebutjat + pendent + pendent d\'aprovació + Tots els missatges nous d\'aquests/es membres s\'amagaran! + Blocar membres per a tots/es? + Condicions actualitzades + Error en llegir la contrasenya de la base de dades + Els/les membres s\'eliminaran del xat; això no es pot desfer! + Els missatges d\'aquests/es membres es mostraran! + Desblocar membres per a tots/es? + moderació + 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. + Els/les membres s\'eliminaran del grup; això no es pot desfer! + 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. + Expulsar membres? + Política de privadesa i condicions d\'ús. + Els xats privats, els grups i els vostres contactes no són accessibles per als operadors de servidor. + Acceptar + En utilitzar SimpleX Chat accepteu:\n- enviar només contingut legal en grups públics.\n- Respectar els altres usuaris, sense correu brossa. + Configurar els operadors de servidor + Enllaç al canal SimpleX + Aquest enllaç requereix una versió de l\'aplicació més recent. Actualitzeu l\'aplicació o demaneu al vostre contacte que enviï un enllaç compatible. + Enllaç de connexió no compatible + Emprar enllaços curts (BETA) + Enllaç complet + Enllaç curt + Tots els servidors + Apagat + Feu servir el port TCP 443 només per a servidors predefinits. + Servidors predefinits + 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 cd642f8828..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,7 +8,7 @@ Přidat přednastavené servery Pokročilá nastavení sítě Přijmout - Přidat server… + Přidat server 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. Přijmout Povolit svým kontaktům odesílat mizící zprávy. @@ -55,7 +55,6 @@ Aktualizovat nastavení sítě\? Inkognito Váš náhodný profil - Uložit barvu Obnovit barvu Zbarvení Povolujete @@ -73,10 +72,10 @@ Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání). (24 hodin) Nevratné mazání zpráv je v tomto chatu zakázáno. Přímé zprávy členům zakázány. - %d sec + %d s %ds %d min - %d hodinu + %d hodina nabízeno %s: %2s Odkazy na skupiny Hlasové zprávy @@ -209,7 +208,7 @@ Požadavek na připojení byl odeslán! Jednorázová pozvánka Bezpečnostní kód - %s je ověřeno + %s ověřen Chat konzole SMP servery Přednastavená adresa serveru @@ -225,11 +224,10 @@ Jak používat servery Vaše servery ICE Konfigurace serverů ICE - Nastavení sítě + Pokročilé nastavení Použít proxy server SOCKS\? Použít přímé připojení k internetu\? Ne - Onion hostitelé nebudou použiti. Chat profil Připojení simplexmq: v%s (%2s) @@ -245,7 +243,7 @@ Decentralizovaná Jak to funguje Jak funguje SimpleX - 2 vrstvého koncového šifrování.]]> + Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy. Soukromé oznámení Pravidelné Ignorovat @@ -282,7 +280,6 @@ Tuto akci nelze vzít zpět! Váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny. Restartujte aplikaci a vytvořte nový chat profil. 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ů. - Zastavte chat a povolte akce s databází. Soubory a média Smazat soubory a média\? Odstranit zprávy @@ -305,8 +302,6 @@ Obnovte zálohu databáze Po obnovení zálohy databáze zadejte předchozí frázi. Tuto akci nelze vrátit zpět. Chat je zastaven - Chat se archivuje - Smazat archiv chatu? Připojit se ke skupině\? Připojte se na Opustit @@ -325,7 +320,7 @@ Zabezpečit frontu Okamžitá oznámení! V nastavení ji lze vypnout - oznámení se budou zobrazovat pokud aplikace běží.]]> - povolte pro SimpleX běh na pozadí v dalším dialogu. Jinak budou oznámení vypnuta.]]> + Povolte v příštím dotazu okamžité přijímání notifikací.]]> 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. Je vyžadována přístupová fráze Chcete-li dostávat oznámení, zadejte přístupovou frázi do databáze. @@ -348,17 +343,15 @@ volání… připojen ukončen - Příští generace soukromé komunikace - Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte. + Budoucnost soukromé komunikace + Rozhodněte, kdo se může připojit. špatný kontrolní součet zprávy Databáze chatu importována Nová přístupová fráze… Uložte přístupovou frázi a otevřete chat - ARCHIV CHATU Nebyl vybrán žádný kontakt Snažíte se pozvat kontakt, se kterým jste sdíleli inkognito profil, do skupiny, ve které používáte svůj hlavní profil Skupina - Vrátit Aktualizací nastavení se klient znovu připojí ke všem serverům. Nastavit 1 den Chyba spojení (AUTH) @@ -391,7 +384,7 @@ Smazat adresu Celé jméno: Váš současný profil - SimpleX službu na pozadí - denně využije několik procent baterie.]]> + SimpleX běží na pozadí místo používání oznámení.]]> Pravidelná oznámení SimpleX Chat služba Příjem zpráv… @@ -437,7 +430,7 @@ Upravit obrázek Smazat obrázek chyba volání - Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli. + Servery může provozovat kdokoli. Vytvořte si svůj profil Vytvořte si soukromé připojení Videohovor šifrovaný e2e @@ -453,7 +446,7 @@ špatné ID zprávy duplicitní zpráva Přeskočené zprávy - Ochrana osobních údajů a zabezpečení + Soukromí a zabezpečení Vaše soukromí Skrývat aplikaci Odesílat náhledy odkazů @@ -465,7 +458,7 @@ odešel Vyčistit Přepnout - Role bude změněna na \"%s\". Všichni ve skupině budou informováni. + Role bude změněna na "%s". Všichni ve skupině budou informováni. Chyba při odebrání člena Chyba při ukládání profilu skupiny vteřiny @@ -475,7 +468,7 @@ Hlasové zprávy Vy i váš kontakt můžete nevratně mazat odeslané zprávy. (24 hodin) %dm - %dmth + %dm %d hodin %dh %dd @@ -581,7 +574,7 @@ Připojení prostřednictvím odkazu Pokud jste dostali SimpleX Chat pozvánku, můžete ji otevřít v prohlížeči: Pokud zvolíte odmítnutí, odesílatel NEBUDE upozorněn. - Otevřete v mobilní aplikaci, potom klikněte na Připojit.]]> + Otevřete v mobilní aplikaci, potom v aplikaci klepněte na Připojit.]]> Odmítnout Smazat chat Vyčistit @@ -611,7 +604,7 @@ Vložit Tento řetězec není odkazem na připojení! Otevřít v mobilní aplikaci.]]> - %s není ověřeno + %s neověřen Návod k použití Nápověda k markdown Uložit servery @@ -630,18 +623,14 @@ Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní. Uložit Síť a servery - Aktualizovat nastavení hostitelů .onion\? Použít hostitele .onion Když bude dostupný Povinné Onion hostitelé budou použiti, pokud jsou k dispozici. Onion hostitelé nebudou použiti. - Onion hostitelé budou použiti, pokud jsou k dispozici. - Pro připojení budou vyžadováni Onion hostitelé. Izolace přenosu for each chat profile you have in the app.]]> - Oddělit TCP připojení (a SOCKS pověření) bude použito pro všechny kontakty a členy skupin. -\nUpozornění: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat. + pro všechny kontakty a členy skupin. \nUpozornění: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.]]> Vzhled Verze aplikace Verze aplikace: v%s @@ -673,26 +662,23 @@ obdržel potvrzení… připojování… Nové vymezení soukromí - 1. platforma bez jakýchkoliv uživatelských identifikátorů – soukromá již od návrhu. - Odolná vůči spamu a zneužití - 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ů. - když SimpleX nemá žádný identifikátor uživatelů, jak může doručovat zprávy\?]]> - přijímat zprávy, vaše kontakty – servery, které používáte k zasílání zpráv.]]> - Další informace najdete v našem repozitáři na GitHubu. + Bez uživatelských identifikátorů + Odolná vůči spamu + K ochraně soukromí, SimpleX používá ID pro každý z vašich kontaktů. úložišti GitHub.]]> Použijte chat - Lze změnit později v nastavení. + Jak ovlivňuje baterii Když aplikace běží Okamžité Nejlepší pro baterii. Budete přijímat oznámení pouze když aplikace běží (žádná služba na pozadí).]]> - Dobré pro baterii. Služba na pozadí bude kontrolovat každých 10 minut. Můžete zmeškat hovory nebo naléhavé zprávy.]]> - Využívá více baterie! Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.]]> + Dobré pro baterii. Apka bude kontrolovat zprávy každých 10 minut. Můžete zmeškat volání nebo naléhavé zprávy.]]> + Využívá více baterie! Apka stále běží na pozadí - oznámení se zobrazí okamžitě.]]> Vložte přijatý odkaz Příchozí videohovor Příchozí zvukový hovor %1$s se s vámi chce spojit prostřednictvím videohovoru (nešifrovaného e2e). - zvukový hovor (nešifrováno e2e) + zvukový hovor (nešifrován e2e) Odmítnout Vaše hovory Spojení přes relé @@ -744,9 +730,9 @@ Odstranit všechny soubory Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány. Žádné přijaté ani odeslané soubory - %d soubor(ů) s celkovou velikostí %s + %d soubor(y) s celkovou velikostí %s nikdy - %s vteřin(y) + %s sekund(y) Zprávy Toto nastavení se vztahuje na zprávy ve vašem aktuálním chat profilu. Smazat zprávy po @@ -780,12 +766,9 @@ Obnovit zálohu databáze\? Obnovit Chyba při obnovování databáze - Přístupová fráze nebyla v klíčence nalezena, 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. + 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. Chat můžete spustit v Nastavení / Databáze nebo restartováním aplikace. - Uložit archiv - Smazat archiv pozvánka do skupiny %1$s - Vytvořeno dne %1$s Jste zváni do skupiny. Připojte se k členům skupiny. Připojit se inkognito Připojit ke skupině @@ -805,7 +788,7 @@ pozval %1$s připojen změnil roli %s na %s - změnil svou roli na %s + změnil vaši roli na %s odstraněn %1$s odstranil vás skupina odstraněna @@ -863,7 +846,7 @@ Role Změnit roli Změnit - Role bude změněna na \"%s\". Člen obdrží novou pozvánku. + Role bude změněna na "%s". Člen obdrží novou pozvánku. Chyba při změně role přímo Odesíláno přes @@ -910,22 +893,22 @@ Posílání mizících zpráv zakázáno. Nevratné mazání odeslaných zpráv zakázáno. Hlasové zprávy zakázány. - Členové skupiny mohou posílat mizící zprávy. - Mizící zprávy jsou v této skupině zakázány. - Členové skupiny mohou posílat přímé zprávy. - Přímé zprávy mezi členy jsou v této skupině zakázány. - Členové skupin mohou nevratně mazat odeslané zprávy. (24 hodin) - Nevratné mazání zpráv je v této skupině zakázáno. - Členové skupiny mohou posílat hlasové zprávy. - Hlasové zprávy jsou v této skupině zakázány. + Členové mohou posílat mizící zprávy. + Mizící zprávy jsou zakázány. + Členové mohou posílat přímé zprávy. + Přímé zprávy mezi členy jsou v této skupině zakázány. + Členové mohou nevratně mazat odeslané zprávy. (24 hodin) + Nevratné mazání zpráv je zakázáno. + Členové mohou posílat hlasové zprávy. + Hlasové zprávy jsou zakázány. Smazat za %d měsíc %d měsíců %d den - %d dnů + %d dní %d týden %d týdnů - %dw + %dt nabízeno %s zrušeno %s Co je nového @@ -978,7 +961,7 @@ Další vylepšení již brzy! Nyní mohou správci: \n- mazat zprávy členů. -\n- zakázat členy (role \"pozorovatel\") +\n- zakázat členy (role "pozorovatel") Uložit heslo profilu Ztlumit Chraňte své chat profily heslem! @@ -1055,7 +1038,7 @@ Bez hesla aplikace Nemohli jste být ověřeni; Zkuste to prosím znovu. %d minut - %d vteřin + %d sekund Ihned Zapamatujte si jej nebo bezpečně uložte - neexistuje způsob, jak obnovit ztracené heslo! Zámek SimpleX není povolen! @@ -1080,7 +1063,7 @@ Chyba dešifrování Špatný hash zprávy Špatné ID zprávy - Hash předchozí zprávy se liší. + Kontrolní součet předchozí zprávy se liší. 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. %1$d zpráv se nepodařilo dešifrovat. @@ -1131,7 +1114,7 @@ Chyba načítání podrobností Info Hledat - Otevřít chat profily + Změnit chat profily Historie Přijatá zpráva Poslaná zpráva @@ -1158,7 +1141,7 @@ Posláno: %s Zmizí: %s Upraveno: %s - %s (aktuální) + %s (aktuálně) Tmavý motiv Import motivu SimpleX @@ -1171,8 +1154,8 @@ Reakce na zprávy jsou v tomto chatu zakázány. Zakázat reakce na zprávy. Zakázat reakce na zprávy. - Členové skupin mohou přidávat reakce na zprávy. - Reakce na zprávy jsou v této skupině zakázány. + Členové mohou přidávat reakce na zprávy. + Reakce na zprávy jsou zakázány. měsíců Zjistit více Sdílet s kontakty @@ -1209,9 +1192,7 @@ minut vteřin Díky uživatelům - překládejte prostřednictvím Weblate! - - 5 minutové hlasové zprávy. -\n- vlastní čas mizení. -\n- historie úprav. + - 5 minutové hlasové zprávy.\n- volitelný čas mizení zpráv.\n- historie úprav. dní hodin týdnů @@ -1245,7 +1226,7 @@ Pokud později adresu odstraníte, o kontakty nepřijdete. Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem. žádný text - Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli. + Během importu došlo k nezávažným chybám: Vypnout\? Oznámení přestanou fungovat až do nového spuštění aplikace Vypnout @@ -1263,9 +1244,7 @@ Opravit nepodporované členem skupiny Opravit nepodporované kontaktem Zachovat vaše připojení - - více stabilní doručovaní zpráv. -\n- o trochu lepší skupiny. -\n- a více! + - stabilnější doručování zpráv.\n- trochu lepší skupiny.\n- a více! šifrování povoleno pro %s vyžadováno opětovné vyjednávání šifrování pro %s Odesílání potvrzení o doručení je vypnuto pro %d kontakty. @@ -1284,9 +1263,9 @@ Odoblíbit Přerušit změnu adresy Povolit odesílání souborů a médii. - Členové skupiny mohou posílat soubory a média. + Členové mohou posílat soubory a média. Pouze majitelé skupiny mohou povolit soubory a média. - Soubory a média jsou zakázány v této skupině. + Soubory a média jsou zakázány. Vypnout (zachovat přepsání) Povolit pro všechny Povolit (zachovat přepisování) @@ -1362,7 +1341,7 @@ %s: %s %s a %s připojen %s, %s a %s připojen - %s, %s a %d další členové připojeni + %s, %s a %d jiných členů připojeno Rozepsáno Zobrazit poslední zprávy Tato skupina má více než %1$d členů, doručenky nejsou odeslány. @@ -1394,9 +1373,7 @@ Zjednodušený režim inkognito Vytvořit nový profil v desktopové aplikaci. 💻 Změnit inkognito při připojování. - - připojit k adresáři skupin (BETA)! -\n- doručenky (až 20 členů). -\n- rychlejší a stabilnější. + - připojení k adresáři skupin (BETA)!\n- doručenky (až 20 členů).\n- rychlejší a stabilnější. odeslat přímou zprávu smazaný kontakt Chyba @@ -1407,7 +1384,7 @@ Adresa počítače Připojit se do skupiny? Skupina již existuje! - %s připojený + %s připojen Toto zařízení Počítač Jméno tohoto zařízení @@ -1446,8 +1423,8 @@ %1$s.]]> Propojit mobilní a stolní aplikace! 🔗 To je váš vlastní jednorázový odkaz! - %d zpráv označeno jako smazaná - Prostřednictvím zabezpečeného kvant rezistentního protokolu. + %d zpráv označeno jako smazané + Díky kvantům odolnému protokolu zabezpečení. Použijte z PC v aplikaci telefonu a naskenujte QR kód.]]> Již připojuji! Skrýt nežádoucí zprávy. @@ -1489,7 +1466,7 @@ Připojit s %1$s? Blokovat Odblokovat člena? - %d zpráv zablokováno + %d zpráv blokováno Chcete-li povolit telefonu připojení k PC, otevřete tento port ve vašem firewallu, pokud je povolen Blokovat člena Použít z PC @@ -1506,18 +1483,16 @@ Ověřit kód v telefonu Otevřít port ve firewallu Zadejte jméno tohoto zařízení… - Sdílíte neplatnou cestu souboru. Nahlaste problém vývojářům aplikace. + Sdíleli jste neplatnou cestu souboru. Nahlaste problém vývojářům aplikace. Odpojit mobilní telefony Blokovat člena? - %d skupinových událostí + %d událostí skupiny %1$s!]]> Ověřit kód s PC Skenovat QR kód z PC Odblokovat Objevitelný přes lokální síť - - volitelně oznámení odstraněným kontaktům. -\n- profilová jména s mezeramy. -\n- a více! + - volitelné oznámení odstraněným kontaktům.\n- profilová jména s mezeramy.\n- a více! Neplatná cesta souboru Již jste požádali o spojení přes tuto adresu! Zobrazit konzoli v novém okně @@ -1560,7 +1535,7 @@ Provedení funkce trvá příliš dlouho: %1$d vteřin: %2$s Připojit se ke skupině blokováno adminem - %d zpráv zablokováno správcem + %d zpráv blokováno správcem Chyba vytváření zprávy Chyba odstranění soukromých poznámek Smazat soukromé poznámky? @@ -1588,7 +1563,7 @@ člen %1$s změněn na %2$s blokováno Blokováno adminem - Vytvořeno v: %s + Vytvořen v: %s Zpráva příliš velká %s z důvodu: %s]]> Spojení zastaveno @@ -1752,7 +1727,7 @@ Souběžné přijímání SimpleX odkazy Povoleno pro - Členové skupiny mohou odesílat SimpleX odkazy. + Členové mohou odesílat SimpleX odkazy. Zvuky v hovoru Zdroje zpráv zůstávají důvěrné. Správa sítě @@ -1768,6 +1743,641 @@ Kvantům odolné šifrování Kamera Kamera a mikrofon - SimpleX odkazy jsou v této skupině zakázány. + SimpleX odkazy jsou zakázány. koncovým šifrováním s dokonalým dopředným utajením, odmítnutím a obnovením po vloupání.]]> - \ No newline at end of file + Pokročilé nastavení + Všechny barevné režimy + Překročená kapacita - příjemci neobdrží dříve poslané zprávy. + Vždy + Použít na + Potvrdit soubory z neznámých serverů. + Tmavý mód + Téma aplikace + Vždy užít soukromé směrování. + Povolit downgrade + Kopírovat chybu + Barvy chatu + Téma chatu + Další zbarvení 2 + Černé + Mód barvy + Tmavé + Mód tmavých barev + Informace o frontě zpráv + Světlý mód + Upravtesi svůj chat, aby vypadal jinak! + Chyba cílového serveru: %1$s + Chyba: %1$s + Předávací server: %1$s +\nChyba cílového serveru: %2$s + Předávací server: %1$s +\nChyba: %2$s + Upozornění doručování zpráv + Problémy se sítí - zpráva vypršela po mnoha pokusech o odeslání. + Soukromé směrování + Soubor nebyl nalezen - s největší pravděpodobností byl soubor odstraněn nebo zrušen. + Chyba souboru serveru: %1$s + Nelze odeslat zprávu + Chyba souboru + NEpoužívat soukromé směrování. + Záložní směrování zpráv + Režim přeposílání zpráv + Nikdy + Chyba inicializace WebView. Aktualizujte systém na novou verzi. Prosím kontaktujte vývojáře. +\nChyba: %s + Ochrana IP adresy + Status souboru + Status zprávy + Status souboru: %s + Stav zprávy: %s + žádné + Vyplnit + Opakovat + Obnovit uživatelské téma + Obnovit téma aplikace + Bezpečné přijímání souborů + Nové motivy chatu + Soukromé směrování zpráv 🚀 + Chraňte vaši IP adresu před relé zpráv vašich kontaktů.\nPovolte v nastavení *Síť & servery*. + Vylepšené doručování zpráv + Perské UI + 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. + Ne + NEposílejte zprávy přímo, i když váš nebo cílový server nepodporuje soukromé směrování. + SOUBORY + SOUKROMÉ SMĚROVÁNÍ ZPRÁV + Téma profilu + Přijata odpověď + Obnovit barvu + Dobré odpoledne! + Odebrat obrázek + Dobré ráno! + Světlé + Špatný klíč nebo neznámé spojení - pravděpodobně je spojení smazáno. + Špatný klíč nebo neznámá adresa části souboru - soubor je pravděpodobně odstraněn. + Nechráněno + Použít soukromé směrování s neznámými servery. + Použit soukromé směrování s neznámými servery, když IP adresa není chráněna. + Bez Tor nebo VPN bude vaše IP adresa viditelná souborovým serverům. + Ladit doručování + Snížena spotřeba baterie. + Neznámé servery! + Bez Tor nebo VPN bude vaše IP adresa viditelná pro tyto XFTP relé: +\n%1$s. + Když je IP adresa skryta + Ano + Zbarvení tapety + Pozadí tapety + Beta + volání + Rozmazat media + Kontaktu nelze volat + Všechny profily + pokusy + Aktualizace aplikace je stažena + Aktivní spojení + Povolit volání? + Volání zakázáno! + Nelze zavolat člena skupiny + Archivované kontakty + Archivujte kontakty pro pozdější chatování. + Adresa předávacího serveru je nekompatibilní s nastavením sítě: %1$s. + Verze předávacího serveru je nekompatibilní s nastavením sítě: %1$s. + Cílová adresa serveru %1$s je nekompatibilní s nastavením přeposílajícího serveru %2$s. + Chyba připojení k přeposílajícímu serveru %1$s. Prosím, zkuste to později. + 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. + Vybrané nastavení chatu zakazuje tuto zprávu. + Jiné SMP servery + Nastavené SMP servery + Probíhá + Části nahrány + %1$d chyba souboru:\n%2$s + %1$d jiná chyba souboru(ů). + Chyba přeposílaní zpráv + Adresa serveru není kompatibilní s nastavením sítě. + Předat %1$s zpráv(u)? + Nic k předání! + Předat zprávy bez souborů? + %1$d soubor(y) se nepodařilo stáhnout. + %1$d soubor(y) nestažen(y). + %1$d soubor(y) smazán(y). + %1$s zprávy nepředány + Stáhnout + Předávám %1$s zpráv + Předat zprávy… + Uložit %1$s zpráv + Nepoužívat autorizaci s proxy. + Chyba ukládání proxy + Ujistěte se, že nastavení proxy je správné. + Heslo + Proxy autentizace + Instalovat aktualizace + Otevřít umístění souboru + Stahování aktualizace, nezavírejte aplikaci + Prosím restartujte aplikaci. + Instalovány úspěšně + Připomenout později + Zkontrolovat aktualizace + Vypnuto + CHAT DATABÁZE + vypnut + info fronty serveru: %1$s\n\nposlední obdržená zpráva: %2$s + Uložit a připojit znovu + Hrajte ze seznamu chatů. + Přijatých zprávy + Znovu připojit servery? + Kompletní + Zabezpečeno + Prosím zkontrolujte, že SimpleX odkaz je správný. + Chybný odkaz + Verze cílového serveru %1$s je nekompatibilní s nastavením přeposílajícího serveru %2$s. + Zpráva předána + Udržujte konverzaci + Jen smazat konverzaci + Potvrdit smazání kontaktu? + Kontakt bude smazán - nelze vrátit! + Konverzace odstraněna! + Vložit odkaz + Chat databáze exportována + Členu skupiny nelze odeslat zprávu + Požádejte váš kontakt ať povolí volání. + Odeslaných odpovědí + Škálovat + Přizpůsobit + Rozmazání pro lepší soukromí. + Připojte se k vašim přátelům rychleji. + Smazat až 20 zpráv najednou. + Zvětšit velikost písma. + Stav připojení a serverů. + Kontrolujte svou síť + Chyby + Připojen + Připojování + Připojené servery + Dříve připojené servery + Potvrzeno + duplikáty + Smazán + Otevřít nastavení serveru + Nový zážitek z chatu 🎉 + Nové možnosti médií + Nová zpráva + Skenovat / Vložit odkaz + Žádné filtrované kontakty + Chyba inicializace WebView. Ujistěte se, že máte nainstalován WebView podporující architekturu arm64.\nChyba: %s + Chrání vaši IP adresu a připojení. + Zprávy byly odstraněny poté, co jste je vybrali. + Nové přihlašovací údaje SOCKS budou použity pokaždé, když zapnete aplikaci. + Nové přihlašovací údaje SOCKS budou použity pro každý server. + Znovu připojte všechny připojené servery pro vynucení doručení. Využívá další provoz. + Resetovat všechny tipy + %1$d soubor(y) se stále stahují. + Lepší datování zpráv. + Lepší zabezpečení ✅ + Části odstraněny + připojení + Aktuální profil + Chyba znovu připojení serveru + Chyba při opětovném připojování serverů + Znovu připojte server pro vynucení doručení. Využívá další provoz. + Chyba + Zprávy budou smazány - nelze vrátit! + Smazat bez upozornění + hledat + Chyba přepínání profilu + Vyberte chat profil + zpráva + otevřít + Kontakt smazán! + neaktivní + Detaily + Resetovat všechny statistiky + Prosím zkuste později. + Chyba soukromého směrování + Adresa serveru není kompatibilní s nastavením sítě: %1$s. + Člen neaktivní + Zpráva může být doručena později až bude člen aktivní. + Zatím bez přímého spojení, zpráva je předána adminem. + Připojování ke kontaktu, počkejte nebo se podívejte později! + Kontakt odstraněn. + Chyby mazání + Podrobné statistiky + Části staženy + Server + Odesílat zprávy přímo, když je IP adresa chráněna a váš nebo cílový server nepodporuje soukromé směrování. + Odeslat zprávy přímo, když váš nebo cílový server nepodporuje soukromé směrování. + Zkontrolovat aktualizace + Vypnout + Vypnut + Stáhnout %s (%s) + Pozvat + Vytvořit + Roh + Tvar zpráv + Pokračovat + Klikněte na info tlačítko blízko pole adresy, pro použití mikrofonu. + Otevřete nastavení Safari / Webové stránky / mikrofon, vyberte možnost Povolit pro localhost. + Velikost písma + Stáhnout nové verze z GitHubu. + Dosažitelný panel nástrojů chatu + Odebrat archiv? + Soubory + Odeslaných zpráv + Staženo + Znovu připojit server? + chyba dešifrování + Lepší volání + Větší přívětivost + Přizpůsobitelný tvar zpráv. + Smazat nebo moderovat až 200 zpráv. + Předat až 20 zpráv najednou. + Žádné info, zkuste načíst znovu + Informace o serverech + Stažené soubory + Chyby stahování + Chyba resetování statistik + Přijaté zprávy + Přijato celkem + Chyb přijmutí + Připojte znovu všechny servery + Reset + Resetovat všechny statistiky? + Odeslané zprávy + Odeslaných celkem + Adresa serveru + Dosažitelné panely nástrojů + Chyba potvrzení + Připojení + Vytvořen + prošlý + jiné + jiné chyby + Znovu připojit + Chyby odesílání + Odesláno přímo + Odeslaných přes proxy + Odstranit %d zpráv členů? + Zprávy budou označeny pro smazání. Příjemci budou moci tyto zprávy odhalit. + Vybrat + Zpráva + Nic nevybráno + Vybrány %d + Střední + Příjem zpráv + Nastavené XFTP servery + Servery médií a souborů + Servery zpráv + Jiné FXTP servery + Pozvat + Pošlete zprávu pro povolení volání. + Přijmout podmínky + Přijaté podmínky + Přidat seznam + Vidí to pouze odesílatelé a moderátoři + Vidite to pouze vy a moderátor + archivovaná hlášení + Chyba v konfiguraci serverů. + Chyba při příjímání podmínek + Žádné servery pro odesílání souborů. + Pokračovat + Otevřít změny + Nenalezen žádný chat + archivovaná hlášení podle %s + přijmuté pozvání + Porušení pokynů komunity + Nevhodný obsah + Jiný důvod + Chyba změny serverů + Nevhodný profil + Pro profil chatu %s: + Žádné chatovací servery. + Žádné servery pro soukromé směrování chatů. + Žádné servery pro příjem souborů. + Žádné servery pro příjem zpráv. + Všechny chaty budou ze seznamu odebrány %s, a seznam bude smazán + Pro sociální sítě + Vzdálené telefony + %s.]]> + %s.]]> + %s.]]> + Pro soukromé směrování + Otevřít podmínky + Nebo importovat soubor archivu + Obsah porušuje podmínky používání + Spojení blokováno + Připojení je blokováno serverovým operátorem:\n%1$s. + Archivovat hlášení? + Žádná zpráva + Archivovat + Archivovat hlášení + Vymazat hlášení + Soubor je blokován operátorem serveru:\n%1$s. + Nahlásit + Žádné nepřečtené chaty + Žádné chaty + Žádné chaty v seznamu %s. + Kontakty + Oblíbené + Vše + Nahlásit obsah: uvidí ho pouze moderátoři skupiny. + Otevřít pomocí %s + Vytvořit seznam + Přidat prátele + Ztlumit vše + Název seznamu a emoji by mělo být různé pro všechny seznamy. + Změnit seznam + Název seznamu... + Povolit logování + Síťoví operátoři + Otevřít odkaz ze seznamu chatu + Otevřít webový odkaz? + Zeptat se + Ne + Otevřít odkaz + Chyba ukládání databáze + Přidat členy týmu + Chat bude smazán pro všechny členy - tato akce je nevratná! + Chat bude pro vás smazán - tato akce je nevratná! + Opustit chat + Člen bude odstraněn z chatu - tato akce je nevratná! + Připojení není připraveno. + Podmínky budou automaticky přijaty pro povolené operátory dne: %s. + Síťový operátor + %s.]]> + Přednastavené servery + Text aktuálních podmínek se nepodařilo načíst, podmínky si můžete prohlédnout prostřednictvím tohoto odkazu: + Podmínky budou přijaty dne: %s. + %s.]]> + %s.]]> + Přidány servery pro média & soubory + Povolte Flux v nastavení sítě a serverů pro lepší ochranu metadat. + Servery přes proxy + Soukromí pro vaše zákazníky. + moderátor + Přidány chatovací servery + Operátor + Zakázat automatické mazání zpráv? + Vymazat zprávy chatu z tohoto zařízení. + Zakázat mazání zpráv + %s, přijměte podmínky používání.]]> + Xiaomi zařízení: aby fungovaly notifikace, povolte prosím Autostart v systémovém nastavení.]]> + Zpráva je moc velká! + Zmenšete prosím velikost zprávy a odešlete ji znovu. + Zmenšete prosím velikost zprávy nebo odeberte média a odešlete ji znovu. + pouze s jedním kontaktem - sdílejte osobně nebo přes jakoukoliv chatovací službu.]]> + Zabezpečení připojení + Aplikace vždy poběží na pozadí + Upozornění a baterie + Například, pokud váš kontakt dostane zprávu pres Chat server SimpleX, vaše aplikace ji doručí pomocí Flux serveru. + Chyba načtení seznamů chatu + Chyba vytváření seznamu chatu + Chyba aktualizace seznamu chatů + Firmy + Dosažitelný panel nástrojů chatu + Pozvat do chatu + Pouze vlastníci chatu mohou upravit předvolby. + Chat + Podmínky použití + Chyba přidání serveru + Panel nástrojů aplikace + Rozmazání + Přímé zprávy mezi členy jsou zakázány. + pro lepší ochranu metadat. + Chat již existuje + Přes proxy + Firemní chaty + Vylepšená navigace chatu + - Otevřít chat na první nepřečtené zprávě.\n- Přejít na citované zprávy. + Archivovat všechna hlášení? + Archivovat %d hlášení? + Pro všechny moderátory + Pro mě + %d reportů + Skupiny + Seznam + Hlášení členů + Poznámky + Zprávy v tomto chatu nebudou nikdy smazány. + Změnit řazení + Vymazat + Vymazat seznam? + Upravit + Chyba ukládání nastavení + Nebo sdílet soukromě + Firemní adresa + Podmínky přijaté dne: %s. + Přímé zprávy mezi členy jsou v tomto chatu zakázány. + Členové mohou zprávy nahlásit moderátorům. + Zakázat nahlašování zpráv moderátorům. + Chyba aktualizace serveru + Operátor serveru + Decentralizace sítě + Archivovat hlášení + Opustit chat? + Nastavení adres + Adresa nebo jednorázový odkaz? + Přidat na seznam + Přidat členy týmu ke konverzaci + Všechna hlášení vám budou archivována + Povolit nahlašování zpráv moderátorům + Změnit automatické mazání zpráv? + Vytvořit jednorázový odkaz + Jak to pomáhá soukromí + Vymazat chat + Vymazat chat? + %s.]]> + %s.]]> + %1$s.]]> + Relace aplikace + Podmínky budou přijaty pro povolené operátory po 30 dnech. + Připojení vyžaduje opětovné vyjednání šifrování. + Probíhá opětovné vyjednávání o šifrování. + Opravit + Opravit připojení? + Nový server + koncovým šifrováním, s post-quantovým zabezpečením v přímých zprávách.]]> + Žádné služba na pozadí + Kontrolovat zprávy každých 10 minut + Chyba vytváření hlášení + 1 rok + výchozí (%s) + Použijte náhodné přihlašovací údaje + Nahlásit spam: uvidí pouze skupinový moderátoři. + Přečíst podmínky + Webové stránky + Odebírán + požádáno o připojení + Spam + Žádné mediální a souborové servery. + Chyba dočasného souboru + Přesunout sezení + TCP připojení + Použité servery + Použit %s + Pro příjem + Systém + Spam + Nedoručené zprávy + Tato zpráva byla smazána, nebo dosud nebyla přijata. + Důvod nahlášení? + Report bude archivován. + Zprávy budou pro všechny členy označeny jako moderované. + Reporty + Nahlašte profil člena: uvidí pouze skupinový moderátoři. + Nahlásit porušení: Uvidí pouze moderátoři skupiny. + Zobrazit stav zpráv + Přeskočit tuto verzi + Zobrazit seznam v novém okně + Aktualizovat aplikaci automaticky + Některé soubory nebyly exportovány + Nepřečtené zmínky + Sdílet adresu veřejně + Sdílent SimpleX adresu na sociálních médiích. + Sdílejte 1 rázový odkaz s přítelem + SOCKS Proxy + Dostupná aktualizace: %s + Stabilní + Můžete nastavit operátory v nastavení sítě a serverů. + Ocas + Zastavíte přijímání zpráv z tohoto chatu. Chat historie bude zachována. + Servery pro nové soubory vašeho aktuálního chat profilu + Protokolu serveru se změnil. + Operátor serveru se změnil. + Zoom + Nastavit výchozí téma + Nahráno + Ano + Přepnout chat seznam: + Tuto akci nelze zrušit - zprávy odeslané a přijaté v tomto chatu dříve než vybraná, budou smazány. + Statistiky serverů budou obnoveny - nemůže být vráceno! + Odešlete soukromý report + Pomozte administrátorům moderovat své skupiny. + Rychlejší mazání skupin. + Od %s. + Můžete zmínit až %1$s členů ve zprávě! + Musíte kopírovat a snížit velikost zprávy, abyste ji poslali. + Jméno + SimpleX adresa nebo 1 rázový odkaz? + Nastavení + Uložit seznam + Stažení aktualizace zrušeno + Můžete uložit exportovaný archiv. + Použit pro zprávy + Server přidán k operátoru %s. + Průhlednost + Přepínání chat profilu pro 1-rázové pozvánky. + video + Sdílet profil + Reportování zpráv je zakázáno v této skupině. + XFTP servr + Nahrané soubory + Odběr ignorován + Verze serveru není kompatibilní s nastavením sítě. + Pro pozdější vytvoření adresy, klepněte v menu na Vytvořit SimpleX adresu. + Vaše servery + Zobrazit aktualizované podmínky + 1 report + zamítnuto + Report: %s + Nahlásit další: uvidí pouze skupinový moderátoři. + Můžete nastavit název připojení, pro pamatování, s kým byl odkaz sdílen. + 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. + Pro každý profil použijte různé přihlašovací údaje + Neznámé servery + Pro ochranu vaší IP adresy, soukromé směrování používá vaše servery SMP k doručování zpráv. + Použít web portu + TCP port pro zprávy + Váš chat profil bude zaslán členům + Režim systému + Zmínky členů 👋 + Organizujte konverzace do seznamů + Lepší výkon skupin + Lepší soukromí a bezpečnost + Nenechte si ujít důležité zprávy. + Rychlejší odesílání zpráv. + Vlastní názvy souborů médií. + Zobrazuji informace pro + Chyby nahrávání + Soukromé směrování se používá k doručování zpráv těmto serverům, protože k nim nejste připojeni. + Velikost + Přečíst později + Aktualizovat + %s servery + Celkem + Verze serveru není kompatibilní s vaší aplikací: %1$s. + Připojení dosáhlo limitu nedoručených zpráv, váš kontakt je asi offline. + Pro odeslílání + Použit pro soubory + Druhý přednastavený operátor v aplikaci! + Chcete-li volat, povolte použití mikrofonu. Ukončete hovor a zkuste to znovu. + Statistiky + Chyby odběru + O operátorech + SimpleX Chat a Flux udělali dohodu, a zahrnuly servery spravované Flux do aplikace. + SMP server + Vaše kontakty + Od %s.\nJsou všechna data uchovávána ve vašem zařízení. + Serverový operátoři + Vybrat provozovatele sítě pro použití. + Můžete nakonfigurovat servery v nastavení. + Zvuk ztlumen + Používat aplikaci jednou rukou. + SinpleX protokoly přezkoumány Trail Bits. + Přepnínání zvuku a videa během hovoru. + Vaše přihlašovací údaje mohou být zaslány nešifrované. + Pokud je povolen více než jeden operátor, nikdo z nich nemá metadata, aby poznal, kdo s kým komunikuje. + Nastavit název chatu.. + Použít TCP port %1$s, když není zadán žádný port. + Tento odkaz byl použit s jiným mobilním zařízením, vytvořte na počítači nový odkaz. + Získejte upozornění, když jste zmíněni. + SimpleX adresa a 1 rázové odkazy je bezpečné sdílet přes všechny komunikátory. + Zprávy budou smazány pro všechny členy. + Aplikace vyžaduje potvrzení stahování z neznámých serverů (s výjimkou .onion nebo při aktivaci SOCKS proxy). + Musíte povolit kontaktům volání, abyste jim mohli zavolat. + Nastavení expirace zpráv. + Zobrazit procenta + Nahraný archiv databáze bude ze serverů trvale odstraněn. + Pro ochranu před záměnou odkazů, můžete porovnat bezpečnostní kódy. + Stále si můžete prohlédnout rozhovor s %1$s v chat seznamu. + Aplikace chrání vaše soukromí pomocí různých operátorů v každé konverzaci. + Můžete jej změnit v nastavení Vzhledu. + Role se změní na %s. Každý v chatu bude upozorněn. + Chcete-li být informováni o nových verzích, zapněte periodickou kontrolu pro Stabilní nebo Beta verze. + Pro každé připojení použijte různé přihlašovací údaje. + Silný + Zobrazit podmínky + Můžete posílat zprávy %1$s z archivovaných kontaktů. + Můžete přenést exportovanou databázi. + zamítnuto + Jemný + Chyba čtení přístupové fráze databáze + 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. + 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. + Aktualizované podmínky + čekám na schválení + čekám + Blokovat členy všem? + Všechny nové zprávy od těchto členů budou skryty! + Odblokovat členy všem? + moderátoři + Zprávy od těchto členů budou zobrazeny! + Členové budou odstraněny ze skupiny - toto nelze zvrátit! + Odebrat členy? + Členové budou odstraněny z chatu - toto nelze zvrátit! + Použitím SimpleX chatu souhlasíte že:\n- ve veřejných skupinách budete zasílat pouze legální obsah.\n- budete respektovat ostatní uživatele – žádný spam. + Přijmout + Nastavit operátora serveru + Zásady ochrany soukromí a podmínky používání. + Soukromé konverzace, skupiny a kontakty nejsou přístupné provozovatelům serverů. + Nepodporovaný odkaz k připojení + Používejte krátké odkazy (BETA) + Tento odkaz vyžaduje novější verzi aplikace. Prosím aktualizujte aplikaci nebo požádejte kontakt o odeslání kompatibilního odkazu. + odkaz SimpleX kanálu + Úplný odkaz + Krátký odkaz + Všechny servery + Vypnut + Přednastavené servery + Použít TCP port 443 jen pro přednastavené servery. + 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 6f4afec2f3..6646720c5c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -20,7 +20,7 @@ Gelöscht als gelöscht markiert Das Senden von Dateien wird noch nicht unterstützt - Der Empfang von Dateien wird noch nicht unterstützt + Das Herunterladen von Dateien wird noch nicht unterstützt Profil Unbekanntes Nachrichtenformat Ungültiges Nachrichtenformat @@ -58,11 +58,11 @@ Fehler beim Senden der Nachricht Fehler beim Hinzufügen von Mitgliedern Fehler beim Beitritt zur Gruppe - Datei kann nicht empfangen werden + Datei kann nicht heruntergeladen werden Der Absender hat die Dateiübertragung abgebrochen. - Fehler beim Empfangen der Datei + Fehler beim Herunterladen der Datei Fehler beim Erstellen der Adresse - Kontakt ist bereits vorhanden + Kontakt besteht bereits Sie sind bereits mit %1$s verbunden. Ungültiger Verbindungslink Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben, oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden. @@ -74,7 +74,7 @@ Fehler beim Löschen des Kontakts Fehler beim Löschen der Gruppe Fehler beim Löschen der Kontaktanfrage - Fehler beim Löschen der anstehenden Kontaktaufnahme + Fehler beim Löschen der ausstehenden Kontaktaufnahme Fehler beim Wechseln der Empfängeradresse Der Test ist beim Schritt %s fehlgeschlagen. Um Warteschlangen zu erzeugen, benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort. @@ -88,9 +88,9 @@ Sofortige Benachrichtigungen Sofortige Benachrichtigungen! Sofortige Benachrichtigungen sind deaktiviert! - SimpleX-Hintergrunddienst genutzt werden – dieser benötigt ein paar Prozent Akkuleistung am Tag.]]> + läuft SimpleX im Hintergrund ab, anstatt Push-Benachrichtigungen zu nutzen.]]> Diese können über die Einstellungen deaktiviert werden – solange die App läuft, werden Benachrichtigungen weiterhin angezeigt.]]> - Erlauben Sie SimpleX im Hintergrund abzulaufen. Ansonsten werden die Benachrichtigungen deaktiviert.]]> + Erlauben Sie es im nächsten Dialog.]]> 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. Periodische Benachrichtigungen Periodische Benachrichtigungen sind deaktiviert! @@ -157,7 +157,7 @@ Verbergen Erlauben Die Nachricht löschen? - Nachricht wird gelöscht – dies kann nicht rückgängig gemacht werden! + Nachricht wird gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachricht wird zum Löschen markiert. Der/die Empfänger kann/können diese Nachricht aufdecken. Für mich löschen Für alle @@ -195,10 +195,10 @@ Bild Warten auf ein Bild - Es wird um den Empfang eines Bildes gebeten + Es wird um das Herunterladen eines Bildes gebeten Bild gesendet Warten auf ein Bild - Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach! + Das Bild wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach! Bild wurde im Fotoalbum gespeichert Datei @@ -206,7 +206,7 @@ Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%1$s). Die derzeit maximal unterstützte Dateigröße beträgt %1$s. Warte auf Datei - Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später noch mal nach! + Die Datei wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später noch mal nach! Datei gespeichert Datei nicht gefunden Fehler beim Speichern der Datei @@ -218,7 +218,7 @@ Benachrichtigungen Kontakt löschen? - Der Kontakt und alle Nachrichten werden gelöscht – dies kann nicht rückgängig gemacht werden! + Es wird der Kontakt und alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Kontakt löschen Kontaktname festlegen… Verbunden @@ -264,7 +264,7 @@ Danke, dass Sie SimpleX Chat installiert haben! mit den SimpleX-Chat-Entwicklern verbinden, um Fragen zu stellen und aktuelle Informationen zu erhalten.]]> Um einen neuen Chat zu starten - Schaltfläche antippen + Schaltfläche tippen Danach die gewünschte Aktion auswählen: Über Link verbinden Wenn Sie einen SimpleX-Chat-Einladungslink erhalten haben, können Sie ihn in Ihrem Browser öffnen: @@ -277,11 +277,11 @@ Inkognito akzeptieren Ablehnen - Chatinhalte löschen? - Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. - Löschen - Chatinhalte löschen - Chatinhalte löschen + Chat-Inhalte entfernen? + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. + Entfernen + Chat-Inhalte entfernen + Chat-Inhalte entfernen Löschen Löschen Als gelesen markieren @@ -298,7 +298,8 @@ Die von Ihnen akzeptierte Verbindung wird abgebrochen! Ihr Kontakt ist noch nicht verbunden! - 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). + 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. möchte sich mit Ihnen verbinden! @@ -354,8 +355,8 @@ Chat-Konsole SMP-Server Voreingestellte Serveradresse - Füge voreingestellte Server hinzu - Füge Server hinzu… + Voreingestellte Server hinzufügen + Server hinzufügen Teste Server Teste alle Server Alle Server speichern @@ -390,23 +391,19 @@ Speichern Netzwerk & Server Erweiterte Netzwerkeinstellungen - Netzwerkeinstellungen + Erweiterte Einstellungen SOCKS-Proxy verwenden? Zugriff auf die Server über SOCKS-Proxy auf Port %d? Der Proxy muss gestartet werden, bevor diese Option aktiviert wird. Direkte Internetverbindung verwenden? Wenn Sie dies bestätigen, können die Messaging-Server Ihre IP-Adresse sowie Ihren Provider sehen und mit welchen Servern Sie sich verbinden. - Einstellung für .onion-Hosts aktualisieren? Verwende .onion-Hosts Wenn verfügbar Nein Erforderlich - Onion-Hosts werden verwendet, wenn sie verfügbar sind. + Wenn Onion-Hosts verfügbar sind, werden sie verwendet. Onion-Hosts werden nicht verwendet. Für die Verbindung werden Onion-Hosts benötigt. \nBitte beachten Sie: Ohne .onion-Adresse können Sie keine Verbindung mit den Servern herstellen. - Onion-Hosts werden verwendet, wenn sie verfügbar sind. - Onion-Hosts werden nicht verwendet. - Für die Verbindung werden Onion-Hosts benötigt. Erscheinungsbild Adresse erstellen @@ -465,23 +462,20 @@ Verbunden Beendet - Die nächste Generation von privatem Messaging + Die Zukunft des Messagings Datenschutz neu definiert - Die erste Plattform ohne Benutzerkennungen – privat per Design - Immun gegen Spam und Missbrauch - Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen. + Keine Benutzerkennungen. + Immun gegen Spam + Sie entscheiden, wer sich mit Ihnen verbinden kann. Dezentral - Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen. + Jeder kann seine eigenen Server aufsetzen. Erstellen Sie Ihr Profil Stellen Sie eine private Verbindung her Wie es funktioniert Wie SimpleX funktioniert - Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?]]> - 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. - empfangen und an Ihre Kontakte senden wollen.]]> - zweischichtige Ende-zu-Ende-Verschlüsselung gesendet werden.]]> - Erfahren Sie in unserem GitHub-Repository mehr dazu. + SimpleX nutzt individuelle Kennungen für jeden Ihrer Kontakte, um Ihre Privatsphäre zu schützen. + Nur die Endgeräte speichern Benutzerprofile, Kontakte, Gruppen und Nachrichten. GitHub-Repository mehr dazu.]]> Fügen Sie den erhaltenen Link ein @@ -502,7 +496,7 @@ Audio- & Videoanrufe Ihre Anrufe - Über ein Relais verbinden + Immer über ein Relais verbinden Anrufe auf Sperrbildschirm: Akzeptieren Anzeigen @@ -531,7 +525,7 @@ Lautsprecher an Kamera umdrehen - Anstehender Anruf + Ausstehender Anruf Verpasster Anruf Abgelehnter Anruf Anruf wird verbunden @@ -550,7 +544,7 @@ \n3. Die Verbindung wurde kompromittiert. Datenschutz & Sicherheit - Ihre Privatsphäre + Privatsphäre App-Bildschirm schützen Bilder automatisch akzeptieren Link-Vorschau senden @@ -591,31 +585,29 @@ Fehler beim Beenden des Chats Fehler beim Exportieren der Chat-Datenbank Chat-Datenbank importieren? - 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. + 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! Importieren Fehler beim Löschen der Chat-Datenbank Fehler beim Importieren der Chat-Datenbank Chat-Datenbank importiert Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden. Chat-Profil löschen? - Diese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. + Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden! Chat-Datenbank gelöscht Starten Sie die App neu, um ein neues Chat-Profil zu erstellen. 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. - Chat beenden, um Datenbankaktionen zu erlauben. Dateien und Medien löschen? - 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. - Keine empfangenen oder gesendeten Dateien + 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! + Keine herunter- oder hochgeladenen Dateien %d Datei(en) mit einem Gesamtspeicherverbrauch von %s nie - täglich - wöchentlich - monatlich + Älter als ein Tag + Älter als eine Woche + Älter als ein Monat %s Sekunde(n) - Löschen der Nachrichten + Nachrichten löschen Automatisches Löschen von Nachrichten aktivieren? - 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. + 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! Nachrichten löschen Fehler beim Ändern der Einstellung @@ -666,20 +658,14 @@ Der Versuch, das Passwort der Datenbank zu ändern, konnte nicht abgeschlossen werden. Datenbanksicherung wiederherstellen Datenbanksicherung wiederherstellen? - Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden. + Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden! Wiederherstellen Fehler bei der Wiederherstellung der Datenbank - 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. + 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. Der Chat wurde beendet Sie können den Chat über die App-Einstellungen/Datenbank oder durch Neustart der App starten. - Datenbank-Archiv - CHAT-ARCHIV - Archiv speichern - Archiv löschen - Erstellt am %1$s - Chat-Archiv löschen\? Einladung zur Gruppe %1$s Der Gruppe beitreten? @@ -696,7 +682,7 @@ Die Einladung ist abgelaufen! Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde. Die Gruppe wurde nicht gefunden! - Diese Gruppe existiert nicht mehr. + Diese Gruppe ist nicht mehr vorhanden. Kontakte können nicht eingeladen werden! 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. @@ -755,7 +741,7 @@ Mitgliedereinladungen überspringen Kontakte auswählen Kontakt geprüft - Löschen + Entfernen %d Kontakt(e) ausgewählt Keine Kontakte ausgewählt Kontakt kann nicht eingeladen werden! @@ -766,8 +752,8 @@ Sie: %1$s Gruppe löschen Gruppe löschen? - Die Gruppe wird für alle Mitglieder gelöscht – dies kann nicht rückgängig gemacht werden! - Die Gruppe wird für Sie gelöscht – dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! + Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden! Gruppe verlassen Gruppenprofil bearbeiten Gruppen-Link @@ -786,7 +772,7 @@ Mitglied entfernen Direktnachricht senden - Das Mitglied wird aus der Gruppe entfernt – dies kann nicht rückgängig gemacht werden! + Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! Entfernen MITGLIED Rolle @@ -794,8 +780,8 @@ Ändern Wechseln Die Mitgliederrolle ändern? - Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt. - Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten. + Die Rolle wird auf %s geändert. Alle Mitglieder der Gruppe werden benachrichtigt. + Die Rolle wird auf %s geändert. Das Mitglied wird eine neue Einladung erhalten. Fehler beim Entfernen des Mitglieds Fehler beim Ändern der Rolle Gruppe @@ -825,7 +811,6 @@ Protokollzeitüberschreitung PING-Intervall TCP-Keep-Alive aktivieren - Zurückkehren Speichern Netzwerkeinstellungen aktualisieren? Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden. @@ -842,7 +827,6 @@ Dunkel Design - Farbe speichern Farben zurücksetzen Akzent @@ -857,7 +841,7 @@ Chat-Präferenzen Kontakt-Präferenzen Gruppen-Präferenzen - Gruppen-Präferenzen einstellen + Gruppen-Präferenzen festlegen Ihre Präferenzen Direkte Nachrichten Für jeden löschen @@ -887,22 +871,22 @@ Unwiederbringliches Löschen von Nachrichten nicht erlauben. Das Senden von Sprachnachrichten erlauben. Das Senden von Sprachnachrichten nicht erlauben. - Gruppenmitglieder können Direktnachrichten versenden. - In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. - Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen (bis zu 24 Stunden). - In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt. - Gruppenmitglieder können Sprachnachrichten versenden. - In dieser Gruppe sind Sprachnachrichten nicht erlaubt. + Mitglieder können Direktnachrichten versenden. + In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. + Mitglieder können gesendete Nachrichten unwiederbringlich löschen (bis zu 24 Stunden). + Das unwiederbringliche Löschen von Nachrichten ist nicht erlaubt. + Mitglieder können Sprachnachrichten versenden. + Sprachnachrichten sind nicht erlaubt. LIVE Schauen Sie sich den Sicherheitscode an Sofort - Gute Option für die Batterieausdauer. Der Hintergrundservice überprüft alle 10 Minuten nach Nachrichten. Sie können eventuell Anrufe oder dringende Nachrichten verpassen.]]> + Gute Option für die Batterieausdauer. Die App prüft alle 10 Minuten auf neue Nachrichten. Sie können eventuell Anrufe oder dringende Nachrichten verpassen.]]> Beste Option für die Akkulaufzeit. Sie empfangen Benachrichtigungen nur, solange die App läuft (kein aktiver Hintergrundservice).]]> Senden %s wurde erfolgreich überprüft Verifikation zurücknehmen Solange die App abläuft - Kann später über die Einstellungen geändert werden. + Auswirkung auf den Akku Löschen nach %d Stunde %d Stunden @@ -928,18 +912,18 @@ Periodisch Erlauben Sie das Senden von verschwindenden Nachrichten. In diesem Chat sind verschwindende Nachrichten nicht erlaubt. - Nur Sie können verschwindende Nachrichten senden. - Nur Ihr Kontakt kann verschwindende Nachrichten senden. + Nur Sie können verschwindende Nachrichten versenden. + Nur Ihr Kontakt kann verschwindende Nachrichten versenden. Fehler beim Laden des Chats Fehler beim Laden der Chats Bitte aktualisieren Sie die App und nehmen Sie Kontakt mit den Entwicklern auf. - Benötigt mehr Leistung Ihrer Batterie! Der Hintergrundservice läuft permanent ab. Benachrichtigungen werden Ihnen angezeigt, sobald Sie neue Nachrichten erhalten haben.]]> + Benötigt mehr Leistung Ihrer Batterie! Die App läuft permanent im Hintergrund ab. Benachrichtigungen werden Ihnen sofort angezeigt.]]> Gruppenlink erstellen Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten. Das Senden von verschwindenden Nachrichten nicht erlauben. - In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt. - Gruppenmitglieder können verschwindende Nachrichten senden. - Fügen Sie Server durch Scannen der QR-Codes hinzu. + Verschwindende Nachrichten sind nicht erlaubt. + Mitglieder können verschwindende Nachrichten versenden. + Server durch Scannen von QR-Codes hinzufügen. Verschwindende Nachrichten Übernehmen Einen Tag festlegen @@ -952,7 +936,7 @@ Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen. Private Benachrichtigungen Chat verwenden - Ihr Kontakt und Sie können beide verschwindende Nachrichten senden. + Ihr Kontakt und Sie können beide verschwindende Nachrichten versenden. %dh Gruppen-Links Neu in %s @@ -989,11 +973,11 @@ App-Version: v%s Core Version: v%s Profil hinzufügen - Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden! + Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Chat-Profil löschen für PING-Zähler Transport-Isolations-Modus aktualisieren\? - Mögliche Server für neue Verbindungen über Ihr aktuelles Chat-Profil + Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil Dateien & Medien Transport-Isolation Chat-Profil löschen\? @@ -1002,9 +986,7 @@ Verbindung Chat-Profil Dateien für alle Chat-Profile löschen - Für jeden Kontakt und jedes Gruppenmitglied wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt. -\n -\nBitte beachten Sie: Wenn Sie viele Verbindungen haben, können Akkuverbrauch und Datennutzung wesentlich höher ausfallen und einige Verbindungen scheitern. + Für jeden Kontakt und jedes Gruppenmitglied wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.\nBitte beachten Sie: Wenn Sie viele Verbindungen haben, können Akkuverbrauch und Datennutzung wesentlich höher ausfallen und einige Verbindungen scheitern.]]> Für jedes von Ihnen in der App genutzte Chat-Profil wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.]]> Nur lokale Profildaten Profil und Serververbindungen @@ -1065,7 +1047,7 @@ Bluetooth-Unterstützung und weitere Verbesserungen. Administratoren können nun \n- Nachrichten von Gruppenmitgliedern löschen -\n- Gruppenmitglieder deaktivieren („Beobachter“-Rolle) +\n- Gruppenmitglieder deaktivieren (Beobachter-Rolle)
Gruppen-Begrüßungsmeldung Weiter reduzierter Batterieverbrauch Weitere Verbesserungen sind bald verfügbar! @@ -1078,7 +1060,7 @@ Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind. Begrüßungsmeldung Sie können ein Benutzerprofil verbergen oder stummschalten – für das Menü gedrückt halten. - Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Ihre Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen. + Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite "Ihre Chat-Profile" ein, um Ihr verborgenes Profil zu sehen. Migrations-Bestätigung ungültig Aktualisieren und den Chat öffnen Datenbank-Aktualisierungen bestätigen @@ -1088,29 +1070,29 @@ Datenbank-Aktualisierung Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s Datenbank herabstufen und den Chat öffnen - Inkompatible Datenbank-Version + Datenbank-Version nicht kompatibel Warnung: Sie könnten einige Daten verlieren! Datenbank auf alte Version herabstufen Datenbank-IDs und Transport-Isolationsoption. Die Datenbank-Version ist neuer als die App, keine Abwärts-Migration für: %s Verberge: Migrationen: %s - Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist. - Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist. + Das Bild wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist. + Die Datei wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist. Chat-Profil löschen Profil löschen Verbergen des Profils aufheben Passwort für Profil Verbergen des Chat-Profils aufheben - Aufforderung zum Empfang des Videos + Aufforderung zum Herunterladen des Videos Es können nur 10 Videos zur gleichen Zeit versendet werden Zu viele Videos auf einmal! Video Video gesendet - Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat. + Das Video wird heruntergeladen, sobald Ihr Kontakt das Hochladen beendet hat. Auf das Video warten Auf das Video warten - Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später! + Das Video wird heruntergeladen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später! Ihre XFTP-Server Host Fehler beim Speichern der XFTP-Server @@ -1155,7 +1137,7 @@ SOCKS-Proxy nutzen SimpleX-Sperrmodus SimpleX-Sperre ist nicht aktiviert! - Verwende .onion-Hosts auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.]]> + Verwende .onion-Hosts auf "Nein", wenn der SOCKS-Proxy sie nicht unterstützt.]]> Bestätigen System Sie können nicht überprüft werden – bitte versuchen Sie es nochmal. @@ -1177,11 +1159,11 @@ Sowohl Sie als auch Ihr Kontakt können Anrufe tätigen. Nur Sie können Anrufe tätigen. Audio-/Video-Anrufe nicht erlauben. - Den Empfang der Datei beenden\? - Das Senden der Datei beenden\? - Der Empfang der Datei wird beendet. + Herunterladen der Datei beenden? + Das Hochladen der Datei beenden? + Das Herunterladen der Datei wird beendet. Das Senden der Datei wird beendet. - Datei beenden + Download beenden Die Datei wird von den Servern gelöscht. Widerrufen Datei widerrufen @@ -1208,7 +1190,7 @@ Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen. Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen. Design anpassen - DESIGN-FARBEN + INTERFACE-FARBEN Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können. @@ -1248,7 +1230,7 @@ Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link. Benutzeranleitung.]]> 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. - Offene Chat-Profile + Chat-Profile wechseln Sie können Ihre Adresse als Link oder QR-Code teilen – jede Person kann sich mit Ihnen verbinden. Werden die App-Daten komplett gelöscht. Es wurde ein leeres Chat-Profil mit dem eingegebenen Namen erstellt und die App öffnet wie gewohnt. @@ -1264,8 +1246,8 @@ Selbstzerstörung aktivieren Wenn Sie diesen Zugangscode während des Öffnens der App eingeben, werden alle App-Daten unwiederbringlich gelöscht! Selbstzerstörungs-Zugangscode - Zugangscode einstellen - In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt. + Zugangscode festlegen + Reaktionen auf Nachrichten sind nicht erlaubt. Fehler beim Laden von Details Empfangene Nachricht Information @@ -1296,7 +1278,7 @@ Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben. Reaktionen auf Nachrichten erlauben. Reaktionen auf Nachrichten nicht erlauben. - Gruppenmitglieder können eine Reaktion auf Nachrichten geben. + Mitglieder können eine Reaktion auf Nachrichten geben. Mehr erfahren Endlich haben wir sie! 🚀 Reaktionen auf Nachrichten @@ -1311,9 +1293,7 @@ Farbdesigns anpassen und weitergeben. Tage Stunden - - Bis zu 5 Minuten lange Sprachnachrichten -\n- Zeitdauer für verschwindende Nachrichten anpassen -\n- Nachrichtenverlauf bearbeiten + - Bis zu 5 Minuten lange Sprachnachrichten\n- Zeitdauer für verschwindende Nachrichten anpassen\n- Nachrichtenverlauf bearbeiten benutzerdefiniert Monate Auswählen @@ -1324,27 +1304,27 @@ Gelöscht um: %s Verschwindet um Verschwindet um: %s - Nachrichtenverlauf bearbeiten + Nachrichtenverlauf In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt. Kein Text - Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten – weitere Details finden Sie in der Chat-Konsole. + Während des Imports sind nicht schwerwiegende Fehler aufgetreten: Herunterfahren\? Bis zum Neustart der App erhalten Sie keine Benachrichtigungen mehr APP Neustart Herunterfahren - Fehler beim Abbrechen des Adresswechsels - Abbrechen + Fehler beim Beenden des Adresswechsels + Beenden Dateien und Medien Das Senden von Dateien und Medien erlauben. Das Senden von Dateien und Medien nicht erlauben. - Wechsel der Empfängeradresse abbrechen - Wechsel der Empfängeradresse abbrechen? + Wechsel der Empfängeradresse beenden + Wechsel der Empfängeradresse beenden? Dateien und Medien sind nicht erlaubt! Nur Gruppenbesitzer können Dateien und Medien aktivieren. - Gruppenmitglieder können Dateien und Medien senden. - Der Wechsel der Empfängeradresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet. - In dieser Gruppe sind Dateien und Medien nicht erlaubt. + Mitglieder können Dateien und Medien versenden. + Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet. + Dateien und Medien sind nicht erlaubt. Favorit entfernen Favorit Keine gefilterten Chats @@ -1386,7 +1366,7 @@ Nach ungelesenen und favorisierten Chats filtern. Das Senden von Empfangsbestätigungen an alle Kontakte in allen sichtbaren Chat-Profilen wird aktiviert. Das Senden von Bestätigungen an %d Kontakte ist deaktiviert - Diese Einstellungen gelten für Ihr aktuelles Profil + Diese Einstellungen gelten für Ihr aktuelles Chat-Profil Sie können in den Kontakt- und Gruppeneinstellungen überschrieben werden. Kontakte Bestätigungen deaktivieren\? @@ -1401,10 +1381,8 @@ Wir haben das zweite Häkchen vermisst! ✅ Reparatur der Verschlüsselung nach Wiedereinspielen von Backups. Ein paar weitere Dinge - Auch wenn sie im Chat deaktiviert sind. - - stabilere Zustellung von Nachrichten. -\n- ein bisschen verbesserte Gruppen. -\n- und mehr! + Auch wenn sie in den Unterhaltungen deaktiviert sind. + - Stabilere Zustellung von Nachrichten.\n- Ein bisschen verbesserte Gruppen.\n- Und mehr! Nicht aktivieren Das Senden von Empfangsbestätigungen an alle Kontakte wird aktiviert. Sie können diese später in den Datenschutz- und Sicherheits-Einstellungen der App aktivieren. @@ -1430,8 +1408,8 @@ An dieses Gruppenmitglied wird eine Verbindungsanfrage gesendet. Direkt verbinden\? Inkognito verbinden - Das aktuelle Profil nutzen - Ein neues Inkognito-Profil nutzen + Aktuelles Chat-Profil nutzen + Neues Inkognito-Profil nutzen App-Akkuverbrauch / Unbeschränkt , um Anrufe im Hintergrund zu führen.]]> Fügen Sie den erhaltenen Link ein, um sich mit Ihrem Kontakt zu verbinden… Es wird ein neues Zufallsprofil geteilt. @@ -1460,7 +1438,7 @@ Datenbank-Ordner öffnen Das Passwort wird in Klartext in den Einstellungen gespeichert, nachdem Sie es geändert oder die App neu gestartet haben. Das Passwort wurde in Klartext in den Einstellungen gespeichert. - Bitte beachten Sie: Die Nachrichten- und Dateirelais sind per SOCKS Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]> + Bitte beachten Sie: Die Nachrichten- und Datei-Relais sind per SOCKS-Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]> Lokale Dateien verschlüsseln Öffnen Gespeicherte Dateien & Medien verschlüsseln @@ -1474,9 +1452,7 @@ Arabisch, Bulgarisch, Finnisch, Hebräisch, Thailändisch und Ukrainisch - Dank der Nutzer und Weblate. Erstellen eines neuen Profils in der Desktop-App. 💻 Inkognito beim Verbinden einschalten. - - Verbindung mit dem Directory-Service (BETA)! -\n- Empfangsbestätigungen (für bis zu 20 Mitglieder). -\n- Schneller und stabiler. + - Verbindung mit dem Directory-Service (BETA)!\n- Empfangsbestätigungen (für bis zu 20 Mitglieder).\n- Schneller und stabiler. Direktnachricht senden Direkt miteinander verbunden Erweitern @@ -1507,7 +1483,7 @@ %1$s.]]> Das ist Ihre eigene SimpleX-Adresse! Richtiger Name für %s? - %d Nachrichten löschen? + Sollen %d Nachrichten gelöscht werden? Mit %1$s verbinden? Mitglied entfernen Blockieren @@ -1529,7 +1505,7 @@ Freigeben Ungültiger Datei-Pfad Sie haben über diese Adresse bereits eine Verbindung beantragt! - Die Konsole in einem neuen Fenster anzeigen + Konsole in einem neuen Fenster anzeigen Von %s werden alle neuen Nachrichten ausgeblendet! Blockiert Fehler bei der Neuverhandlung der Verschlüsselung @@ -1555,14 +1531,14 @@ Falsche Desktop-Adresse Geräte Desktop-Verbindung trennen? - Desktop-App-Version %s ist mit dieser App nicht kompatibel. + Die Desktop-App-Version %s ist nicht mit dieser App kompatibel. Neues Mobiltelefon-Gerät Nur ein Gerät kann gleichzeitig genutzt werden Verknüpfe Mobiltelefon- und Desktop-Apps! 🔗 Über ein sicheres quantenbeständiges Protokoll Vom Desktop aus nutzen und scannen Sie den QR-Code.]]> Um unerwünschte Nachrichten zu verbergen. - Inkompatible Version + Version nicht kompatibel (Neu)]]> Desktop entkoppeln? Verknüpfte Desktop-Optionen @@ -1585,9 +1561,7 @@ Desktop-Adresse einfügen Code mit dem Desktop überprüfen Den QR-Code vom Desktop scannen - - Optionale Benachrichtigung von gelöschten Kontakten. -\n- Profilnamen mit Leerzeichen. -\n- Und mehr! + - Optionale Benachrichtigung von gelöschten Kontakten.\n- Profilnamen mit Leerzeichen.\n- Und mehr! Vom Mobiltelefon scannen Verbindungen überprüfen Bitte warten Sie, solange die Datei von dem verknüpften Mobiltelefon geladen wird @@ -1634,7 +1608,7 @@ Zum Scannen tippen Behalten Zum Link einfügen tippen - Suchen oder fügen Sie den SimpleX-Link ein + Suchen oder SimpleX-Link einfügen Der Chat wurde gestoppt. Wenn diese Datenbank bereits auf einem anderen Gerät von Ihnen verwendet wurde, sollten Sie diese dorthin zurück übertragen, bevor Sie den Chat starten. Chat starten? Interne Fehler anzeigen @@ -1670,16 +1644,16 @@ Ehemaliges Mitglied %1$s Die Ausführung dieser Funktion dauert zu lange: %1$d Sekunden: %2$s Langsame Funktion - Zeige langsame API-Aufrufe an + Langsame API-Aufrufe anzeigen unbekannt Optionen für Entwickler unbekannter Gruppenmitglieds-Status Mit verschlüsselten Dateien und Medien. Private Notizen - Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden! - Private Notizen löschen? - %s wurde blockiert - %s wurde freigegeben + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! + Private Notizen entfernen? + es wurde %s blockiert + Es wurden %s freigegeben Sie haben %s blockiert Sie haben %s freigegeben Mitglied für Alle blockieren? @@ -1729,10 +1703,10 @@ Link-Details werden heruntergeladen Archiv wird heruntergeladen Anwenden - Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Server hochgeladen. + Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen. Archivieren und Hochladen Warnung: Das Archiv wird gelöscht.]]> - Überprüfen Sie Ihre Internet-Verbindung und probieren Sie es nochmals + Überprüfen Sie Ihre Internetverbindung und probieren Sie es nochmals Datenbank wird archiviert Bitte beachten Sie: Aus Sicherheitsgründen wird die Nachrichtenentschlüsselung Ihrer Verbindungen abgebrochen, wenn Sie die gleiche Datenbank auf zwei Geräten nutzen.]]> Migration abbrechen @@ -1740,8 +1714,8 @@ Bitte bestätigen Sie für die Migration, dass Sie sich an Ihr Datenbank-Passwort erinnern. Hochladen bestätigen Herunterladen fehlgeschlagen - Ende-zu-Ende-Verschlüsselung mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt.]]> - Quantum-resistente E2E-Verschlüsselung mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt.]]> + Ende-zu-Ende-Verschlüsselung mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt.]]> + Quantum-resistente E2E-Verschlüsselung mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt.]]> Dieser Chat ist durch Ende-zu-Ende-Verschlüsselung geschützt. Dieser Chat ist durch Quantum-resistente Ende-zu-Ende-Verschlüsselung geschützt. Migrationsansicht öffnen @@ -1808,7 +1782,7 @@ weitergeleitet Netzwerkverbindung Keine Netzwerkverbindung - Zellulär + Mobilfunknetz Andere WiFi Kabelgebundenes Netzwerk @@ -1816,12 +1790,12 @@ SimpleX-Links sind nicht erlaubt Sprachnachrichten sind nicht erlaubt SimpleX-Links - Gruppenmitglieder können SimpleX-Links senden. + Mitglieder können SimpleX-Links versenden. Administratoren Alle Mitglieder Aktiviert für Eigentümer - In dieser Gruppe sind SimpleX-Links nicht erlaubt. + SimpleX-Links sind nicht erlaubt. Das Senden von SimpleX-Links nicht erlauben. Das Senden von SimpleX-Links erlauben. Lautsprecher @@ -1850,4 +1824,642 @@ Profil-Bilder Form der Profil-Bilder Quadratisch, kreisförmig oder irgendetwas dazwischen. - \ No newline at end of file + Fehler auf dem Ziel-Server: %1$s + Fehler: %1$s + Warnung bei der Nachrichtenzustellung + Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht. + Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel. + Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen. + Weiterleitungs-Server: %1$s +\nFehler auf dem Ziel-Server: %2$s + Weiterleitungs-Server: %1$s +\nFehler: %2$s + Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen. + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel. + Immer + Privates Routing + Nie + Unbekannte Server + Ungeschützt + Sie nutzen privates Routing mit unbekannten Servern. + Sie nutzen KEIN privates Routing. + Modus für das Nachrichten-Routing + Ja + Nein + Wenn die IP-Adresse versteckt ist + Fallback für das Nachrichten-Routing + Nachrichtenstatus anzeigen + Herabstufung erlauben + Sie nutzen immer privates Routing. + Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Ziel-Server kein privates Routing unterstützt. + PRIVATES NACHRICHTEN-ROUTING + Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Ziel-Server kein privates Routing unterstützt. + Nachrichten werden direkt versendet, wenn Ihr oder der Ziel-Server kein privates Routing unterstützt. + Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt. + Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist. + IP-Adresse schützen + DATEIEN + Die App wird bei unbekannten Datei-Servern nach einer Download-Bestätigung fragen (außer bei .onion oder wenn ein SOCKS-Proxy aktiviert ist). + Unbekannte Server! + Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein. + Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: +\n%1$s. + Profil-Design + Schwarz + Farbvariante + Dunkel + Hell + Farbe zurücksetzen + Gesendete Antwort + System + Dunkle Variante + Füllen + Helle Variante + Empfangene Antwort + Bild entfernen + Wiederholen + Skalieren + Default-Design festlegen + Wallpaper-Akzent + Wallpaper-Hintergrund + Anwenden auf + Zusätzlicher Akzent 2 + Erweiterte Einstellungen + Alle Farbvarianten + Chat-Farben + Chat-Design + Passend + Chat-Liste in einem neuen Fenster anzeigen + Guten Nachmittag! + Guten Morgen! + Farben für die dunkle Variante + App-Design + Persische Bedienoberfläche + Auf das Benutzer-spezifische Design zurücksetzen + Fehler bei der Initialisierung von Webview. Aktualisieren Sie Ihr System auf die neue Version. Bitte kontaktieren Sie die Entwickler. +\nFehler: %s + Auf das App-Design zurücksetzen + Dateien von unbekannten Servern bestätigen. + Verbesserte Zustellung von Nachrichten + Gestalten Sie Ihre Chats unterschiedlich! + Neue Chat-Designs + Privates Nachrichten-Routing 🚀 + Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais , die Ihr Kontakt ausgewählt hat. +\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen. + Dateien sicher herunterladen + Mit reduziertem Akkuverbrauch. + Keine Information + Debugging-Zustellung + Nachrichten-Warteschlangen-Information + Server-Warteschlangen-Information: %1$s +\n +\nZuletzt empfangene Nachricht: %2$s + Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen. + Datei-Server Fehler: %1$s + Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht. + Datei-Fehler + Nachrichten-Status + Nachrichten-Status: %s + Datei-Status + Datei-Status: %s + Temporärer Datei-Fehler + Fehlermeldung kopieren + Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App. + Bitte überprüfen Sie, ob sich das Mobiltelefon und die Desktop-App im gleichen lokalen Netzwerk befinden, und die Desktop-Firewall die Verbindung erlaubt. +\nBitte teilen Sie weitere mögliche Probleme den Entwicklern mit. + Nachricht wurde nicht gesendet + Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. + Bitte versuchen Sie es später erneut. + Fehler beim privaten Routing + Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird. + Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet. + Konfigurierte SMP-Server + Konfigurierte XFTP-Server + Andere SMP-Server + Andere XFTP-Server + Abgeschlossen + Inaktiv + Verbunden + Verbinden + Aktive Verbindungen + Aktuelles Profil + Detaillierte Statistiken + Details + Heruntergeladen + Fehler + Fehler beim Wiederherstellen der Verbindungen zu den Servern + Fehler beim Zurücksetzen der Statistiken + Fehler + Empfangene Nachrichten + Nachrichtenempfang + Ausstehend + Bisher verbundene Server + Proxy-Server + Empfangene Nachrichten + Summe aller empfangenen Nachrichten + Fehler bei der Bestätigung + Versuche + Daten-Pakete gelöscht + Daten-Pakete hochgeladen + Verbindungen + Erstellt + Entschlüsselungs-Fehler + Fehler beim Löschen + Heruntergeladene Dateien + Fehler beim Herunterladen + Duplikate + Abgelaufen + Server-Einstellungen öffnen + Andere Fehler + Proxy + Fehler beim Empfang + Neu verbinden + Deaktiviert + Beta + App-Aktualisierung wird heruntergeladen. App nicht schließen! + Heruntergeladen %s (%s) + Nach Aktualisierungen suchen + Deaktivieren + Erfolgreich installiert + Aktualisierung installieren + Bitte starten Sie die App neu. + Bestätigt + Alle Profile + App-Aktualisierung wurde heruntergeladen + Nach Aktualisierungen suchen + Daten-Pakete heruntergeladen + Verbundene Server + Gelöscht + deaktiviert + Fehler beim Wiederherstellen der Verbindung zum Server + Nachricht weitergeleitet + Dateien + Schriftgröße + Mitglied inaktiv + Gesendete Nachrichten + Keine Information - es wird versucht neu zu laden + Dateispeicherort öffnen + andere + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %1$s. + Link scannen / einfügen + Alle Server neu verbinden + Server neu verbinden? + Alle Server neu verbinden? + Um die Auslieferung von Nachrichten zu erzwingen, werden alle Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt. + Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt. + Zurücksetzen + Alle Statistiken zurücksetzen + Alle Statistiken zurücksetzen? + Gesendete Nachrichten + Summe aller gesendeten Nachrichten + Abgesichert + Fehler beim Senden + Direkt gesendet + Über einen Proxy gesendet + Server-Adresse + Später erinnern + Server-Informationen + Ihre App ist nicht mit der Server-Version kompatibel: %1$s. + Prozentualen Anteil anzeigen + Zoom + SMP-Server + Informationen zeigen für + Beginnend mit %s.\nAlle Daten werden nur auf Ihrem Gerät gespeichert. + Statistiken + Transport-Sitzungen + Hochgeladen + Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt. + Summe aller Abonnements + Die Server-Statistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden! + Größe + Beginnend mit %s. + Abonniert + Fehler beim Abonnieren + Nicht beachtete Abonnements + Hochgeladene Dateien + Fehler beim Hochladen + XFTP-Server + Stabil + Diese Version überspringen + Aktualisierung verfügbar: %s + Aktivieren Sie die periodische Überprüfung auf stabile oder Beta-Versionen der App, um über neue Versionen benachrichtigt zu werden. + Herunterladen der Aktualisierung abgebrochen + Die Weiterleitungs-Server-Adresse ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s. + Die Verbindung des Weiterleitungs-Servers %1$s zum Ziel-Server %2$s schlug fehl. Bitte versuchen Sie es später erneut. + Die Ziel-Server-Version von %1$s ist nicht mit dem Weiterleitungs-Server %2$s kompatibel. + Die Weiterleitungs-Server-Version ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s. + Die Ziel-Server-Adresse von %1$s ist nicht mit den Einstellungen des Weiterleitungs-Servers %2$s kompatibel. + Fehler beim Verbinden zum Weiterleitungs-Server %1$s. Bitte versuchen Sie es später erneut. + Medium verpixeln + Weich + Stark + Mittel + Aus + Verbinden + Nachricht + Öffnen + Chat-Inhalte gelöscht! + Nur die Chat-Inhalte löschen + Suchen + Video + Sie können in der Chat-Liste weiterhin die Unterhaltung mit %1$s einsehen. + Link einfügen + Archivierte Kontakte + Keine gefilterten Kontakte + Ihre Kontakte + App-Symbolleiste unten + Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren. + Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können. + Anrufe erlauben? + Anrufen + Kontakt kann nicht angerufen werden + Anrufe nicht zugelassen! + Löschen des Kontakts bestätigen? + Kontakt gelöscht! + Gruppenmitglied kann nicht angerufen werden + Nachricht an Gruppenmitglied nicht möglich + Verbinde mit Kontakt, bitte warten oder später erneut überprüfen! + Kontakt wurde gelöscht. + Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden! + Ohne Benachrichtigung löschen + Einladen + Chat-Inhalte beibehalten + Nachricht senden, um Anrufe zu aktivieren. + Sie können aus den archivierten Kontakten heraus Nachrichten an %1$s versenden. + Einstellungen + Die Nachrichten werden für alle Gruppenmitglieder gelöscht. + Die Nachrichten werden für alle Mitglieder als moderiert markiert. + Sollen %d Nachrichten von Mitgliedern gelöscht werden? + Nachricht + Nachrichten werden zur Löschung markiert. Der/Die Empfänger hat/haben die Möglichkeit, diese Nachrichten aufzudecken. + Es wurden %d ausgewählt + Es wurde Nichts ausgewählt + Auswählen + Einladen + TCP-Verbindung + Schriftgröße anpassen. + Neue Chat-Erfahrung 🎉 + Die App automatisch aktualisieren + Verbindungs- und Server-Status. + Für bessere Privatsphäre verpixeln. + Kontrollieren Sie Ihr Netzwerk + Neue Medien-Optionen + Speichern und neu verbinden + Erstellen + Laden Sie neue Versionen von GitHub herunter. + Direkt aus der Chat-Liste abspielen. + Chat-Liste umschalten: + Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden. + Kontakte für spätere Chats archivieren. + Ihre IP-Adresse und Verbindungen werden geschützt. + Löschen Sie bis zu 20 Nachrichten auf einmal. + Chat-Symbolleiste unten + Die App mit einer Hand bedienen. + Schneller mit Ihren Freunden verbinden. + Chat-Datenbank wurde exportiert + Weiter + Medien- und Datei-Server + Nachrichten-Server + SOCKS-Proxy + Es wurden nicht alle Dateien exportiert + Sie können die exportierte Datenbank migrieren. + Sie können das exportierte Archiv speichern. + Alle Hinweise zurücksetzen + Neue Nachricht + Bitte überprüfen Sie, ob der SimpleX-Link korrekt ist. + Ungültiger Link + CHAT-DATENBANK + Fehler beim Wechseln des Profils + Die Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! + Profil teilen + System-Modus + Das hochgeladene Datenbank-Archiv wird dauerhaft von den Servern entfernt. + Chat-Profil auswählen + Archiv entfernen? + Ihre Anmeldeinformationen können unverschlüsselt versendet werden. + Verwenden Sie keine Anmeldeinformationen mit einem Proxy. + Ihre Verbindung wurde auf %s verschoben, aber während der Weiterleitung auf das Profil trat ein unerwarteter Fehler auf. + Stellen Sie sicher, dass die Proxy-Konfiguration richtig ist. + Fehler beim Speichern des Proxys + Passwort + Proxy-Authentifizierung + Verwenden Sie für jede Verbindung unterschiedliche Proxy-Anmeldeinformationen. + Verwenden Sie für jedes Profil unterschiedliche Proxy-Anmeldeinformationen. + Verwenden Sie zufällige Anmeldeinformationen + Benutzername + %1$d Datei-Fehler:\n%2$s + %1$d Datei(en) wird/werden immer noch heruntergeladen. + Bei %1$d Datei(en) ist das Herunterladen fehlgeschlagen. + Fehler beim Weiterleiten der Nachrichten + Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten. + Es gibt nichts zum Weiterleiten! + %1$d andere(r) Datei-Fehler. + Soll(en) %1$s Nachricht(en) weitergeleitet werden? + %1$d Datei(en) wurde(n) gelöscht. + %1$d Datei(en) wurde(n) nicht heruntergeladen. + Nachrichten ohne Dateien weiterleiten? + %1$s Nachrichten wurden nicht weitergeleitet + Herunterladen + Es wird/werden %1$s Nachricht(en) weitergeleitet + Nachrichten werden weitergeleitet… + Es wird/werden %1$s Nachricht(en) gesichert + Ton stummgeschaltet + Fehler bei der Initialisierung von WebView. Stellen Sie sicher, dass Sie WebView installiert haben, und es die ARM64-Architektur unterstützt.\nFehler: %s + Form der Nachricht + Sprechblase + Abrundung Ecken + App-Sitzung + Für jeden Server werden neue SOCKS-Anmeldeinformationen genutzt + Server + Klicken Sie auf die Info-Schaltfläche neben dem Adressfeld, um die Verwendung des Mikrofons zu erlauben. + Um Anrufe durchzuführen, erlauben Sie die Nutzung Ihres Mikrofons. Beenden Sie den Anruf und versuchen Sie es erneut. + Öffnen Sie die Safari-Einstellungen / Webseiten / Mikrofon und wählen Sie dann \"Für Localhost erlauben\". + Verbesserte Anrufe + Anpassbares Format des Nachrichtenfelds + Bis zu 200 Nachrichten löschen oder moderieren + Bis zu 20 Nachrichten auf einmal weiterleiten + Die SimpleX-Protokolle wurden von Trail of Bits überprüft. + Während des Anrufs zwischen Audio und Video wechseln + Das Chat-Profil für Einmal-Einladungen wechseln + Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt + Verbesserte Sicherheit ✅ + Verbesserte Nachrichten-Datumsinformation + Verbesserte Nutzer-Erfahrung + Fehler beim Speichern der Server + Keine Nachrichten-Server. + Keine Server für den Empfang von Nachrichten. + Fehler in der Server-Konfiguration. + Für das Chat-Profil %s: + Keine Medien- und Dateiserver. + Keine Server für das Herunterladen von Dateien. + Keine Server für das Versenden von Dateien. + Nicht ausgelieferte Nachrichten + Die SimpleX-Adresse auf sozialen Medien teilen. + Verbindungs-Sicherheit + Den Einmal-Einladungslink mit einem Freund teilen + Die SimpleX-Adresse und Einmal-Links können sicher über beliebige Messenger geteilt werden. + Sie können einen Verbindungsnamen festlegen, um sich zu merken, mit wem der Link geteilt wurde. + Adress-Einstellungen + Einmal-Link erstellen + Für soziale Medien + Oder zum privaten Teilen + SimpleX-Adresse oder Einmal-Link? + Server-Betreiber + Netzwerk-Betreiber + Die App verwendet für jede Unterhaltung Server von unterschiedlichen Betreibern, um Ihre Privatsphäre zu schützen. + Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert. + Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chat-Server empfängt, wird Ihre App diese über einen Flux-Server versenden. + Später einsehen + Wählen sie die zu nutzenden Netzwerk-Betreiber aus. + Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren. + Sie können die Server über die Einstellungen konfigurieren. + Weiter + Aktualisieren + Voreingestellte Server + Nutzungsbedingungen einsehen + Die Nutzungsbedingungen der aktivierten Betreiber werden automatisch akzeptiert am: %s. + Ihre Server + Betreiber + %s Server + Netzwerk-Betreiber + Verwende Server + Webseite + Verwende %s + %s.]]> + %s.]]> + Nutzungsbedingungen + Nutzungsbedingungen anschauen + Nutzungsbedingungen akzeptieren + %s.]]> + Für den Empfang + Für Nachrichten verwenden + Nachrichtenserver hinzugefügt + Für privates Routing + Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil + Für das Senden + Für Dateien verwenden + Fehler beim Hinzufügen des Servers + Änderungen öffnen + Nutzungsbedingungen öffnen + Betreiber-Server + Der Server wurde dem Betreiber %s hinzugefügt. + Der Server-Betreiber wurde geändert. + Das Server-Protokoll wurde geändert. + Transparenz + Für einen besseren Metadatenschutz Flux in den Netzwerk- und Servereinstellungen aktivieren. + Dezentralisiertes Netzwerk + Der zweite voreingestellte Netzwerk-Betreiber in der App! + Verbesserte Chat-Navigation + - Den Chat bei der ersten ungelesenen Nachricht öffnen.\n- Zu zitierten Nachrichten springen. + Aktualisierte Nutzungsbedingungen anschauen + Akzeptierte Nutzungsbedingungen + Medien- und Dateiserver hinzugefügt + Adress- oder Einmal-Link? + App-Symbolleiste + Verpixeln + nur mit einem Kontakt genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger.]]> + %s.]]> + %s.]]> + Die Nutzungsbedingungen wurden akzeptiert am: %s + %s.]]> + %s zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren.]]> + Fehler beim Akzeptieren der Nutzungsbedingungen + Fehler beim Aktualisieren des Servers + für einen besseren Metadatenschutz. + Neuer Server + Keine Nachricht + Keine Server für privates Nachrichten-Routing. + Die Adresse öffentlich teilen + Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. + Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline. + Diese Nachricht wurde gelöscht oder bisher noch nicht empfangen. + Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen. + %s.]]> + %s.]]> + Die Nutzungsbedingungen wurden akzeptiert am: %s. + Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen: + Ferngesteuerte Mobiltelefone + Oder importieren Sie eine Archiv-Datei + Hinweis für Geräte von Xiaomi: Bitte aktivieren Sie in den System-Einstellungen die Option "Autostart", damit Benachrichtigungen funktionieren.]]> + Ende-zu-Ende-verschlüsselt versendet. In Direktnachrichten sogar mit Post-Quantum-Security.]]> + Team-Mitglieder aufnehmen + Freunde aufnehmen + Einladung angenommen + Geschäftliche Adresse + Geschäftliche Chats + Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf. + Die App läuft immer im Hintergrund ab + In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. + Kein Hintergrund-Service + Nachrichten alle 10 Minuten überprüfen + Benachrichtigungen und Akku + Zum Chat einladen + Chat besteht bereits! + Chat-Symbolleiste unten + Chat verlassen + Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden! + Ihr Chat-Profil wird an die Chat-Mitglieder gesendet. + Direktnachrichten zwischen Mitgliedern sind nicht erlaubt. + Wie die Privatsphäre geschützt wird + Chat verlassen? + Chat löschen? + Der Chat wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! + Schutz der Privatsphäre Ihrer Kunden. + Zur Verbindung aufgefordert + Bitte verkleinern Sie die Nachrichten-Größe oder entfernen Sie Medien und versenden Sie diese erneut. + Nur Chat-Eigentümer können die Präferenzen ändern. + Bitte verkleinern Sie die Nachrichten-Größe und versenden Sie diese erneut. + Die Rolle wird auf %s geändert. Im Chat wird Jeder darüber informiert. + Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten. + Sie können die Nachricht kopieren und verkleinern, um sie zu versenden. + Chat löschen + Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! + Die Nachricht ist zu umfangreich! + Wenn mehr als ein Betreiber aktiviert ist, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert. + Chat + %1$s verbunden.]]> + Über die Betreiber + SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen. + Die Verbindung erfordert eine Neuverhandlung der Verschlüsselung. + Die Neuverhandlung der Verschlüsselung läuft. + Reparieren + Verbindung reparieren? + Log-Daten aktivieren + Fehler beim Speichern der Datenbank + Verbindung noch nicht bereit. + Alle + Unternehmen + Fehler beim Erstellen der Chat-Liste + Fehler beim Laden der Chat-Listen + Fehler beim Aktualisieren der Chat-Liste + Favoriten + Liste + Keine Chats + Keine Chats gefunden + Keine Chats in der Liste %s. + Keine ungelesenen Chats + Liste erstellen + Löschen + Liste löschen? + Bearbeiten + Listenname... + Der Listenname und das Emoji sollen für alle Listen unterschiedlich sein. + Liste speichern + Alle Chats werden von der Liste %s entfernt und die Liste wird gelöscht + Gruppen + Kontakte + Liste hinzufügen + Zur Liste hinzufügen + Mit %s öffnen + Anmerkungen + Anordnung ändern + Liste ändern + Fehler beim Erstellen der Meldung + Fehler beim Abspeichern der Einstellungen + Archivierte Meldung + Spam melden: Nur Gruppenmoderatoren werden es sehen. + Melden + Anderer Grund + Meldung archivieren? + Moderator + Inhalt melden: Nur Gruppenmoderatoren werden es sehen. + Unangemessener Inhalt + Unangemessenes Profil + Nur Absender und Moderatoren sehen es + Nur Sie und Moderatoren sehen es + Spam + Archiv + Grund der Meldung? + Die Meldung wird für Sie archiviert. + Mitgliederprofil melden: Nur Gruppenmoderatoren werden es sehen. + Anderes melden: Nur Gruppenmoderatoren werden es sehen. + Verstoß melden: Nur Gruppenmoderatoren werden es sehen. + Meldung archivieren + Meldung löschen + Verstoß gegen die Gemeinschaftsrichtlinien + Archivierte Meldung von %s + Eine Meldung + %d Meldungen + Mitglieder-Meldungen + Meldungen + Inhalt verletzt Nutzungsbedingungen + Spam + Verbindung blockiert + Die Datei wird vom Serverbetreiber blockiert:\n%1$s. + Die Verbindung wird vom Serverbetreiber blockiert:\n%1$s. + Fragen + Nein + Web-Link öffnen + Web-Links aus der Chat-Liste öffnen + Web-Link öffnen? + Ja + Chat-Name festlegen… + 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! + Automatisches Löschen von Nachrichten ändern? + Chat-Nachrichten von Ihrem Gerät löschen + Automatisches Löschen von Nachrichten deaktivieren? + Löschen von Nachrichten deaktivieren + Älter als ein Jahr + Default (%s) + Nachrichten in diesem Chat werden nie gelöscht. + Solange kein Port konfiguriert ist, wird TCP-Port %1$s genutzt. + Web-Port nutzen + TCP-Port für Nachrichtenübermittlung + Sie können bis zu %1$s Mitglieder pro Nachricht erwähnen! + Alle stummschalten + Ungelesene Erwähnungen + Melden von Nachrichten ist in dieser Gruppe nicht erlaubt. + Alle Meldungen archivieren? + Archiviere %d Meldungen? + Für alle Moderatoren + Für mich + Meldungen archivieren + Mitglieder können Nachrichten an Moderatoren melden. + Melden von Nachrichten an Moderatoren nicht erlauben. + Melden von Nachrichten an Moderatoren erlauben. + Alle Meldungen werden für Sie archiviert. + Meldung: %s + Helfen Sie Administratoren bei der Moderation ihrer Gruppen. + Erwähnung von Mitgliedern 👋 + Private Meldungen senden + Bei Erwähnung benachrichtigt werden. + Medien mit anonymisierten Dateinamen. + Verfallsdatum von Nachrichten in Chats festlegen. + Bessere Leistung von Gruppen + Schnelleres löschen von Gruppen + Schnelleres versenden von Nachrichten. + abgelehnt + Bessere(r) Security und Datenschutz + Verpassen Sie keine wichtigen Nachrichten. + Chats in Listen verwalten + abgelehnt + 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. + ausstehend + Fehler beim Lesen des Datenbank-Passworts + Aktualisierte Nutzungsbedingungen + ausstehende Genehmigung + 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. + Mitglieder entfernen? + Mitglieder werden aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden! + Mitglieder werden aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! + Mitglieder für Alle freigeben? + Nachrichten dieser Mitglieder werden angezeigt! + Moderatoren + Mitglieder für Alle blockieren? + Alle neuen Nachrichten dieser Mitglieder werden nicht angezeigt! + Durch die Nutzung von SimpleX Chat erklären Sie sich damit einverstanden:\n- nur legale Inhalte in öffentlichen Gruppen zu versenden.\n- andere Nutzer zu respektieren - kein Spam. + Datenschutz- und Nutzungsbedingungen. + Akzeptieren + Server-Betreiber konfigurieren + Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich. + Verbindungs-Link wird nicht unterstützt + Kurze Links verwenden (BETA) + Verkürzter Link + Vollständiger Link + SimpleX-Kanal-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. + Alle Server + Aus + TCP-Port 443 nur für voreingestellte Server verwenden. + Voreingestellte Server + 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 811257ff39..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 @@ Αποδοχή αιτήματος σύνδεσης; αποδεκτή κλήση Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση. - Προσθήκη διακομιστή… + Προσθήκη διακομιστή Προχωρημένες ρυθμίσεις δικτύου Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών. Οι διαχειριστές μπορούν να δημιουργήσουν τους συνδέσμους συμμετοχής σε ομάδες. @@ -100,7 +100,6 @@ Δεν είναι δυνατή η προετοιμασία της βάσης δεδομένων Ένα νέο τυχαίο προφίλ θα μοιραστεί. Δεν είναι δυνατή η πρόσκληση επαφών! - Αρχείο συνομιλίας Αλλαγή διεύθυνσης λήψης Πιστοποίηση μη διαθέσιμη Αλλαγή @@ -112,7 +111,6 @@ %1$d αποτυχία κρυπτογράφησης μηνύματος αλλαγή διεύθυνσης για %s… Αλλαγή ρόλου ομάδας; - ΑΡΧΕΙΟ ΣΥΝΟΜΙΛΙΑΣ Δεν είναι δυνατή η πρόσκληση επαφής! Αυτόματη αποδοχή αιτήματος επαφής Κλήση… @@ -189,7 +187,6 @@ συνδέεται… Δημιουργία σύνδεσμο ομάδας Σύνδεση σε επιφάνεια εργασίας - Δημιουργήθηκε στις %1$s Συνδεδεμένο στο κινητό Σύνδεση μέσω σύνδεσμο Επαφές @@ -227,4 +224,175 @@ διαγράφτηκε με συντονιστή %s φραγμένος + Σχετικά με τη διεύθυνση SimpleX + συμφωνία κρυπτογράφησης… + Όλες οι χρωματικές λειτουργίες + Η αλλαγή διεύθυνσης θα ακυρωθεί. Θα χρησιμοποιηθεί η παλιά διεύθυνση παραλαβής. + Ενεργές συνδέσεις + Προχωρημένες ρυθμίσεις + Πρόσθετη προφορά + Προσθήκη επαφής + Διακοπή αλλαγής διεύθυνσης + Προχωρημένες ρυθμίσεις + Οι διαχειριστές μπορούν να αποκλείσουν ένα μέλος για όλους. + Αναγνωρισμένο + παραπάνω, λοιπόν: + Προσθέστε τη διεύθυνση στο προφίλ σας, έτσι ώστε οι επαφές σας να μπορούν να τη μοιραστούν με άλλα άτομα. Το ενημέρωμένο προφίλ θα σταλεί στις επαφές σας. + διαχειριστές + Λάθη αναγνώρισης + Προειδοποίηση: το αρχείο θα διαγραφεί.]]> + Υπέρβαση χωρητικότητας - ο παραλήπτης δεν έλαβε μηνύματα που στάλθηκαν προηγουμένως. + αποκλεισμένος από τον διαχειριστή + Συνομιλίες + όλα τα μέλη + Όλες οι επαφές σας θα παραμείνουν ενεργές. Το ανανεωμένο προφίλ σας θα αποσταλεί στις επαφές σας. + Να χρησιμοποιείται πάντα ιδιωτική δρομολόγηση. + Ένα κενό προφίλ συνομιλίας με το παρεχόμενο όνομα δημιουργείται και η εφαρμογή ανοίγει ως συνήθως. + Η βάση δεδομένων της συνομιλίας διαγράφηκε + Απενεργοποίηση ήχου + Eνεργοποίηση ήχου + Κακό μήνυμα hash + Θάμπωση των μέσων + ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ + Το Android Keystore χρησιμοποιείται για την ασφαλή αποθήκευση της φράσης πρόσβασης - επιτρέπει την υπηρεσία ειδοποιήσεων να λειτουργεί. + αποκλεισμένος + Αποκλεισμένος από τον διαχειριστή + Δεν είναι δυνατή η κλήση επαφής + Θέμα εφαρμογής + Εφαρμογή σε + Η εφαρμογή κρυπτογραφεί νέα τοπικά αρχεία (εκτός απο βίντεο). + Καλύτερες ομάδες + Γίνεται ήδη συμμετοχή στην ομάδα! + Αρχειοθέτηση και αποστολή + %1$d διαφορετικό/κα σφάλμα/τα αρχείου/ων. + Η υπηρεσία παρασκηνίου λειτουργεί πάντα - οι ειδοποιήσεις θα εμφανίζονται μόλις τα μηνύματα είναι διαθέσιμα. + %1$d αρχείο/α ακόμα κατεβαίνουν. + %1$d αρχείο/α απέτυχε/χαν να παραληφθεί/ουν + %1$d αρχείο/α διαγράφηκε/καν. + %1$d αρχείο/α δεν κατέβηκε/καν. + %1$s μήνυμα/τα δεν προωθήθηκε/καν + Προφίλ συνομιλίας + για κάθε προφίλ συνομιλίας που έχετε στην εφαρμογή.]]> + Παρακαλώ σημειώστε: οι αναμεταδότες μηνυμάτων και αρχείων συνδέονται μέσω διακομιστή μεσολάβησης SOCKS. Οι κλήσεις και οι προεπισκοπήσεις συνδέσμων αποστολής χρησιμοποιούν άμεση σύνδεση.]]> + Πάντα + Η ενημέρωση της εφαρμογής κατεβαίνει + Έλεγχος για ενημερώσεις + Οποιοσδήποτε μπορεί να φιλοξενήσει διακομιστές. + κλήση ήχου (χωρίς κρυπτογράφηση e2e) + Κλήσεις στην οθόνη κλειδώματος: + Κλήση ήχου + \'Εκδοση Εφαρμογής: %s + Απαγορεύονται οι κλήσεις ήχου/βίντεο. + Αποκλεισμός μελών ομάδας + Τα chunks διαγράφηκαν + Όλα τα δεδομένα διαγράφονται κατά την εισαγωγή. + Αρχειοθετημένες επαφές + Ακύρωση μεταφοράς + Χρώματα συνομιλίας + ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ + Η συνομιλία εκτελείται + Παρακαλώ σημειώστε: ΔΕΝ θα μπορείτε να ανακτήσετε ή να αλλάξετε τη φράση πρόσβασης εάν τη χάσετε.]]> + Αποκλεισμός για όλους + Και εσείς και η επαφή σας μπορείτε να προσθέστε αντιδράσεις μηνυμάτων. + Και εσείς και η επαφή σας μπορείτε να κάνετε κλήσεις. + Επιτρέψτε την αποστολή συνδέσμων SimpleX. + Αραβικά, Βουλγαρικά, Φινλανδικά, Εβραϊκά, Ταϊλανδέζικα και Ουκρανικά - χάρη στους χρήστες και το Weblate. + Μεταφορά δεδομένων εφαρμογής + Θάμπωμα για καλύτερη ιδιωτικότητα. + Η συνομιλία έχει μεταφερθεί! + Αρχειοθέτηση της βάσης δεδομένων + Όλες οι επαφές, συζητήσεις και αρχεία θα κρυπτογραφηθούν με ασφάλεια και θα μεταφορτωθούν σε διαμορφωμένα κομμάτια αναμετάδοσης XFTP. + Κινητή τηλεφωνία + Δημιουργία ομάδας : για την δημιουργίας νέας ομάδας.]]> + Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά + Συζήτηση με τους προγραμματιστές + Ζήτησε να λάβει το βίντεο + Δεν είναι δυνατή η αποστολή μηνυμάτων στο μέλος της ομάδας + Αλλαγή λειτουργίας κλειδώματος + αποκλεισμένος %s + άλλαξε η διεύθυνση για εσάς + και %d άλλες εκδηλώσεις + Μαύρο + Πρόσθετο δευτερεύον + Και εσείς και η επαφή σας μπορείτε να διαγράψετε απεσταλμένα μηνύματα χωρίς ανατροπή. (24 ώρες) + Και εσείς και η επαφή σας μπορείτε να στείλετε ηχητικά μηνύματα. + Η συνομιλία σταμάτησε + Η συνομιλία έχει διακοπεί. Εάν χρησιμοποιήσατε ήδη αυτήν τη βάση δεδομένων σε άλλη συσκευή, θα πρέπει να τη μεταφέρετε πίσω προτού ξεκινήσετε τη συνομιλία. + Η λειτουργία βελτιστοποίησης της μπαταρίας είναι ενεργή, η υπηρεσία παρασκηνίου και τα περιοδικά αιτήματα για νέα μηνύματα θα απενεργοποιηθούν. Μπορείτε να τα ενεργοποιήσετε ξανά μέσω των ρυθμίσεων. + σύνδεσμος μιας χρήσης + Κλήσεις ήχου & βίντεο + Κλήσεις ήχου/βίντεο + Κωδικός εφαρμογής + Συνεδρία εφαρμογής + Η συνομιλία σταμάτησε + Έλεγχος για ενημερώσεις + Κινεζική και Ισπανική διεπαφή + Καλύτερες ημερομηνίες μηνυμάτων + Bluetooth + έντονο + Κονσόλα συνομιλίας + Παρακαλώ σημειώστε: η χρήση της ίδιας βάσης δεδομένων σε δύο συσκευές θα διακόψει την αποκρυπτογράφηση των μηνυμάτων από τις συνδέσεις σας, ως προστασία ασφαλείας.]]> + Χρησιμοποιεί περισσότερη μπαταρία! Η εφαρμογή εκτελείται πάντα στο παρασκήνιο - οι ειδοποιήσεις εμφανίζονται αμέσως.]]> + Η βάση δεδομένων της συνομιλίας εξάχθηκε + κλήση + Κακή διεύθυνση Desktop + Μεταφορά απο άλλη συσκευή στη νέα συσκευή και σαρώστε τον κωδικό QR.]]> + Με προφίλ συνομιλίας (προεπιλογή) ή μέσω σύνδεσης (BETA). + Κάμερα και μικρόφωνο + 6 νέες γλώσσες διεπαφής + Καλό για την μπαταρία. Η εφαρμογή ελέγχει για την παραλαβή μηνυμάτων κάθε 10 λεπτά. Ενδέχεται να χάσετε κλήσεις ή επείγοντα μηνύματα.]]> + Επισύναψη + Διακοπή αλλαγής διεύθυνσης; + Επιλέξτε ένα αρχείο + Όλα τα νέα μηνύνματα απο %s θα αποκρυφθούν! + Δεν είναι δυνατή η λήψη του αρχείου + Πιστοποίηση + Όλα τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί! + Ελέγχει νέα μηνύματα κάθε 10 λεπτά για έως και 1 λεπτό + Η εφαρμογή μπορεί να λαμβάνει ειδοποιήσεις μόνο όταν εκτελείται, καμία υπηρεσία δεν θα ξεκινήσει στο παρασκήνιο + Μπορεί να απενεργοποιηθεί μέσω των ρυθμίσεων – οι ειδοποιήσεις θα εξακολουθούν να εμφανίζονται ενώ η εφαρμογή εκτελείται.]]> + Επιτρέψτε τις επαφές σας να χρησιμοποιούν αντιδράσεις μηνυμάτων. + Και εσείς και η επαφή σας μπορείτε να στείλετε μηνύματα που εξαφανίζονται. + Κάμερα μη διαθέσιμη + Ελέγξτε την διεύθυνση του διακομιστή και δοκιμάστε ξανά. + Επιτρέψτε αντιδράσεις μηνυμάτων εφόσον οι επαφές σας το επιτρέπουν. + %1$d μήνυμα/τα παραλήφθηκε/καν. + Κλήσεις απογορευμένες! + Δεν είναι δυνατή η αποστολή μηνύματος + Η κλήση έχει ήδη τερματιστεί! + Ο κωδικός πρόσβασης της εφαρμογής αντικαθίσταται με κωδικό πρόσβασης αυτοκαταστροφής. + Το Android Keystore θα χρησιμοποιηθεί για την ασφαλή αποθήκευση της φράσης πρόσβασης μετά την επανεκκίνηση της εφαρμογής ή την αλλαγή της φράσης πρόσβασης - θα επιτρέπει τη λήψη ειδοποιήσεων. + Δεν είναι δυνατή η πρόσβαση στο Keystore για αποθήκευση του κωδικού πρόσβασης της βάσης δεδομένων + Αποκλεισμός μέλους + Αποκλεισμός μέλους; + Προτιμήσεις συνομιλίας + Καλύτερα μηνύματα + Εφαρμογή + Συνέναιση υποβάθμισης + Κάμερα + κλήση ήχου + Αρχειοθετήστε τις επαφές για να συνομιλήσετε αργότερα. + Όλα τα προφίλ + %1$d μηνύμα/τα παραλείφθηκε/καν + κακό μήνυμα hash + κακό αναγνωριστικό μηνύματος + Απάντηση κλήσης + Κακό αναγνωριστικό μηνύματος + ΣΥΝΟΜΙΛΙΕΣ + Η βάση δεδεδομένων της συνομιλίας εισάχθηκε + "συμφωνία κρυπτογράφησης για %s…" + Να επιτραπούν οι κλήσεις; + Αποκλεισμός μέλους για όλους; + Κλήσεις ήχου και βίντεο + προσπάθειες + Θέμα συνομιλίας + Καλύτερη ασφάλεια✅ + Καλύτερη εμπειρία χρήστη + Δεν είναι δυνατή η κλήση μέλους ομάδας + Ζήτησε να λάβει την εικόνα + για κάθε επαφή και μέλος ομάδας .\nΛάβετε υπόψη: εάν έχετε πολλές συνδέσεις, η κατανάλωση της μπαταρίας και της κυκλοφορίας μπορεί να είναι σημαντικά υψηλότερη και ορισμένες συνδέσεις μπορεί να αποτύχουν.]]> + Προσθήκη επαφής : για να δημιουργήσετε έναν νέο σύνδεσμο πρόσκλησης ή να συνδεθείτε μέσω ενός συνδέσμου που λάβατε.]]> + Καλύτερο για τη ζωή της μπαταρίας . Θα λαμβάνετε ειδοποιήσεις μόνο όταν εκτελείται η εφαρμογή (ΧΩΡΙΣ υπηρεσία παρασκηνίου).]]> + Beta + Καλύτερες κλήσεις \ 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 54512aca34..5667c42d2d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -4,12 +4,12 @@ Aceptar Configuración avanzada de red Óptimo para la batería. Recibirás notificaciones sólo cuando la aplicación esté abierta (SIN servicio en segundo plano).]]> - Bueno para la batería. El servicio en segundo plano comprueba si hay mensajes cada 10 minutos. Podrías no recibir a tiempo llamadas o mensajes urgentes.]]> + Bueno para la batería. La aplicación comprueba si hay mensajes cada 10 minutos. Podrías perderte llamadas o mensajes urgentes.]]> Aceptar Copia de seguridad de los datos de la aplicación - un dia - un mes - una semana + 1 día + 1 mes + 1 semana Se permiten los mensajes temporales pero sólo si tu contacto también los permite. Añadir servidores mediante el escaneo de códigos QR. Añadir servidores predefinidos @@ -20,7 +20,7 @@ Permites a tus contactos enviar mensajes de voz. siempre La aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano. - ICONO APLICACIÓN + ICONO DE LA APLICACIÓN 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. El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos. Se puede desactivar en Configuración – las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.]]> @@ -29,15 +29,14 @@ y después: ¿Aceptar solicitud de conexión\? Aceptar incógnito - Se eliminarán todos los mensajes SOLO para tí. ¡No podrá deshacerse! - Añadir servidor… + Se eliminarán todos los mensajes SOLO para tí. ¡No puede deshacerse! + Añadir servidor ¿Acceder a los servidores a través del proxy SOCKS en el puerto %d\? El proxy debe iniciarse antes de activar esta opción. Todos tus contactos permanecerán conectados. Apariencia Versión por cada perfil que tengas en la aplicación.]]> - Se usará una conexión TCP (y credenciales SOCKS) independiente por cada contacto y miembro del grupo. -\nRecuerda: si tienes muchas conexiones, el consumo de batería y tráfico pueden ser sustancialmente mayores y algunas conexiones pueden fallar. + por cada contacto y miembro del grupo. \nRecuerda: si tienes muchas conexiones, el consumo de batería y tráfico pueden aumentar bastante y algunas conexiones pueden fallar.]]> a + b Acerca de SimpleX negrita @@ -50,7 +49,7 @@ Audio activado ID de mensaje erróneo Auto aceptar imágenes - Se eliminarán todos los chats y mensajes. ¡No podrá deshacerse! + Se eliminarán todos los chats y mensajes. ¡No puede deshacerse! Aceptar Se permiten mensajes temporales. Android Keystore se usará para almacenar de forma segura la frase de contraseña - permite que el servicio de notificación funcione. @@ -73,28 +72,28 @@ Añadir a otro dispositivo Versión de la aplicación: v%s Solicita recibir la imagen - Recuerda: NO podrás recuperar o cambiar la frase de contraseña si la pierdes.]]> + Recuerda: Si la pierdes NO podrás recuperar o cambiar la frase de contraseña.]]> Tanto tú como tu contacto podéis enviar mensajes de voz. - ¡Consume más batería! El servicio en segundo plano se ejecuta continuamente y las notificaciones se mostrarán de inmediato.]]> + ¡Consume más energía! La aplicación está siempre en segundo plano y las notificaciones se muestran de inmediato.]]> Tanto tú como tu contacto podéis eliminar los mensajes enviados de forma irreversible. (24 horas) Tanto tú como tu contacto podéis enviar mensajes temporales. Crear Crea grupo secreto La contraseña de cifrado de la base de datos será actualizada. ID base de datos - Los mensajes directos entre miembros del grupo no están permitidos. - La contraseña es distinta a la almacenada en Keystore + Los mensajes directos entre miembros del grupo no están permitidos. + La contraseña de la base de datos es diferente a la almacenada en Keystore. La base de datos será cifrada y la contraseña se guardará en Keystore. ¿Eliminar contacto\? ¿Eliminar mensaje\? ¿Eliminar perfil? - grupo eliminado + ha eliminado el grupo ¿Eliminar grupo\? Eliminar en Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo. Desactivar Los mensajes temporales no están permitidos en este chat. - Los mensajes temporales no están permitidos en este grupo. + Mensajes temporales no permitidos. El nombre mostrado no puede contener espacios en blanco. Videollamada con cifrado de extremo a extremo conexión establecida @@ -109,13 +108,13 @@ Llamada con cifrado de extremo a extremo cifrado de extremo a extremo mensaje duplicado - Herramientas desarrollo + Herramientas para desarrolladores Eliminar los archivos de todos los perfiles - Eliminar mensaje + Activar ¡Base de datos cifrada! La base de datos está cifrada con una contraseña aleatoria, puedes cambiarla. - Error base de datos - Para abrir la aplicación se requiere la contraseña de la base de datos + Error en base de datos + Se requiere la contraseña de la base de datos para abrir la aplicación. conectado Crear enlace ¿Eliminar enlace\? @@ -130,14 +129,14 @@ %d semana %d semanas Mensajes temporales - Canfirma tus credenciales + Confirma tus credenciales conectando (presentado) conectando (invitación de presentación ) conectando (aceptado) conectando (anunciado) conexión %1$d Conecta vía enlace / Código QR - El contacto y todos los mensajes serán eliminados. ¡No podrá deshacerse! + El contacto y todos los mensajes serán eliminados. ¡No puede deshacerse! Contacto verificado el contacto dispone de cifrado de extremo a extremo Desconectar @@ -153,13 +152,13 @@ Eliminar en %d seg El contácto ya existe - Error conexión (Autenticación) + Error de conexión (Autenticación) Eliminar para mí Desconectado Conectado Copiado en portapapeles Crea enlace de invitación de un uso. - Escanear código QR ]]> + Escanear código QR ]]> Eliminar Eliminar ¡El contacto aun no se ha conectado! @@ -170,7 +169,7 @@ conectando llamada… Activar llamadas desde la pantalla de bloqueo en Configuración La contraseña de cifrado de la base de datos será actualizada y almacenada en Keystore. - conectando + conectando... creador %d min Tiempo de conexión agotado @@ -191,7 +190,6 @@ conectando… Descentralizada La base de datos será cifrada. - ¿Eliminar archivo del chat\? Crear enlace de grupo Eliminar enlace ¿Eliminar perfil? @@ -210,13 +208,11 @@ Eliminar grupo Editar perfil de grupo Conexión - Eliminar el perfil de chat para + Eliminar el perfil Oscuro %dd %d días ¿Eliminar archivos y multimedia\? - Creado: %1$s - Eliminar archivo conectado directa El contacto permite @@ -242,8 +238,7 @@ Email Conectar Conectar mediante enlace - Base de Datos y -\nContraseña + Base de Datos y Contraseña Contribuye Core versión: v%s Eliminar imagen @@ -271,16 +266,16 @@ No se puede iniciar la base de datos ¿Vaciar chat\? por perfil - Chat está parado + SimpleX está parado rol de %s cambiado a %s Cambiar rol - Mediante perfil (por defecto) o por conexión (BETA) + Mediante perfil (predeterminado) o por conexión (BETA) cambiando de servidor… - Preferencias de Chat + Preferencias generales cancelado %s - Chat está parado + SimpleX está parado LLAMADAS - Chat está en ejecución + SimpleX está en ejecución está cambiando de servidor… habla con los desarrolladores Cancelar vista previa del archivo @@ -289,8 +284,6 @@ Llamada en curso ¿Cambiar contraseña de la base de datos\? No se puede acceder a Keystore para guardar la base de datos de contraseñas - Archivo del chat - ARCHIVOS DE CHAT Cancelar Cancelar mensaje en directo Confirmar @@ -302,71 +295,71 @@ Llamadas en la ventana de bloqueo ¡No se pueden invitar contactos! Consola de Chat - BASE DE DATOS DE CHAT + BASE DE DATOS DE SIMPLEX Base de datos eliminada Base de datos importada Comprueba la dirección del servidor e inténtalo de nuevo. Confirma la contraseña nueva… 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. Imagen guardada en la Galería - El archivo se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde. + El archivo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. Enlace de invitación de un uso Pegar el enlace recibido Error al guardar perfil de grupo Salir sin guardar Archivo guardado - Voltear la cámara + Girar la cámara Invitación de grupo caducada La invitación al grupo ya no es válida, ha sido eliminada por el remitente. - El grupo será eliminado para tí. ¡No podrá deshacerse! + El grupo será eliminado para tí. ¡No puede deshacerse! Cómo usar la sintaxis markdown en modo incógnito mediante enlace de un solo uso Dirección de contacto SimpleX Error al guardar servidores SMP - 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. + 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. Error al actualizar la configuración de red Error al crear dirección Error al eliminar perfil Activar Bloqueo SimpleX - Enlace de invitación de un uso + Enlace de invitación de un solo uso Servidores SMP Características experimentales Error al importar base de datos Error al cambiar configuración Archivo: %s ¡Error al cambiar perfil! - Introduce el servidor manualmente + Añadir manualmente Cómo usar los servidores - Error al parar Chat + Error al parar SimpleX Introduce la contraseña correcta. Introduce la contraseña… Grupo inactivo grupo eliminado - Los miembros del grupo pueden enviar mensajes temporales. + Los miembros pueden enviar mensajes temporales. Enlaces de grupo Enlace de conexión no válido Error al aceptar solicitud del contacto Error al cambiar dirección Error al guardar archivo Error - De la Galería + De la galería Imagen Vídeo Si has recibido un enlace de invitación a SimpleX Chat puedes abrirlo en tu navegador: Si seleccionas rechazar, el remitente NO será notificado. ¡Enlace no válido! - escanear el código QR en la videollamada, o tu contacto puede compartir un enlace de invitación.]]> - muestra el código QR en la videollamada, o comparte el enlace.]]> + escanear el código QR por videollamada, o tu contacto puede compartir un enlace de invitación.]]> + muestra el código QR por videollamada, o comparte el enlace.]]> Cómo Ignorar Error al eliminar base de datos Base de datos cifrada - Error al eliminar miembro - Los miembros del grupo pueden enviar mensajes de voz. + Error al expulsar miembro + Los miembros pueden enviar mensajes de voz. en modo incógnito mediante enlace de dirección del contacto ¡Error al crear perfil! No se pudo cargar el chat - No se pudieron cargar los chats + Fallo en la carga de chats Enlace completo Error al eliminar contacto Error al unirte al grupo @@ -382,12 +375,12 @@ Para todos Archivo Imagen enviada - La imagen se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde. + La imagen se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. vista previa del enlace Error al guardar servidores ICE Servidores ICE (uno por línea) Nombre completo: - Las personas pueden conectarse contigo sólo mediante los enlaces que compartes. + Tu decides quién se conecta. Cómo funciona SimpleX Colgar Archivos y multimedia @@ -412,12 +405,12 @@ SERVIDORES Nombre del grupo: Preferencias del grupo - Los miembros del grupo pueden enviar mensajes directos. - Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas) + Los miembros pueden enviar mensajes directos. + Los miembros pueden eliminar mensajes enviados de forma irreversible. (24 horas) Ocultar pantalla de aplicaciones en aplicaciones recientes. Cifrar Ampliar la selección de roles - El grupo será eliminado para todos los miembros. ¡No podrá deshacerse! + El grupo será eliminado para todos los miembros. ¡No puede deshacerse! Activar TCP keep-alive activado para tí error @@ -434,9 +427,9 @@ ayuda Compartir enlace Cómo funciona - El mensaje será eliminado. ¡No podrá deshacerse! + El mensaje será eliminado. ¡No puede deshacerse! El modo incógnito protege tu privacidad creando un perfil aleatorio por cada contacto. - permite que SimpleX se ejecute en segundo plano en el siguiente cuadro de diálogo. De lo contrario las notificaciones se desactivarán.]]> + Da permiso en el siguiente diálogo para recibir notificaciones instantáneas.]]> Instalar terminal de SimpleX Chat invitación al grupo %1$s ha invitado a %1$s @@ -446,10 +439,9 @@ La base de datos no está cifrada. Escribe una contraseña para protegerla. Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas. Notificación instantánea - Configuración de red - No se usarán hosts .onion - cifrado de extremo a extremo de 2 capas .]]> - Puedes cambiar estos ajustes más tarde en Configuración. + Configuración avanzada + Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes. + Cómo afecta a la batería Instantánea Unirte Unirte en modo incógnito @@ -457,7 +449,7 @@ Claro Activado La eliminación irreversible de mensajes no está permitida en este chat. - La eliminación irreversible de mensajes no está permitida en este grupo. + Eliminación irreversible no permitida. Configuración del servidor mejorada 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. @@ -466,7 +458,6 @@ Contacto y texto MIEMBRO nunca - Se requieren hosts .onion para la conexión No se usarán hosts .onion Vista previa de notificaciones ¡Invitación caducada! @@ -498,7 +489,7 @@ eliminado por el moderador invitación a conectarse ¡Las notificaciones instantáneas están desactivadas! - mensaje nuevo + nuevo mensaje Nueva solicitud de contacto Inicie sesión con sus credenciales Error en la entrega del mensaje @@ -511,9 +502,7 @@ Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas. Se requieren hosts .onion para la conexión \nRecuerda: no podrás conectarte a servidores que no tengan dirección .onion. - Se usarán hosts .onion si están disponibles. - Inmune a spam y abuso - si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes\?]]> + Inmune al spam Videollamada entrante has salido has cambiado de servidor @@ -529,7 +518,7 @@ OK (sólo almacenado por miembros del grupo) Ayuda sintaxis markdown - Servidores y Redes + Servidores y Red Se usarán hosts .onion si están disponibles. cursiva Llamada audio entrante @@ -543,7 +532,7 @@ Contraseña nueva… ¿Unirte al grupo? Entrando al grupo - Error Keystore + Error en Keystore Invitar miembros observador miembro @@ -564,8 +553,8 @@ has cambiado el servidor para %s ha salido Salir del grupo - Sólo los propietarios pueden modificar las preferencias del grupo. - Sólo datos del perfil + Sólo los propietarios del grupo pueden cambiar las preferencias. + Eliminar sólo el perfil no k marcado eliminado @@ -574,7 +563,7 @@ Salir ¿Salir del grupo\? propietario - El miembro será expulsado del grupo. ¡No podrá deshacerse! + El miembro será expulsado del grupo. ¡No puede deshacerse! Sólo los propietarios del grupo pueden activar los mensajes de voz. Más Marcar como verificado @@ -582,13 +571,13 @@ Establecer una conexión privada Comprueba tu conexión de red con %1$s e inténtalo de nuevo. El remitente puede haber eliminado la solicitud de conexión. - Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta + Posiblemente la huella del certificado en la dirección del servidor es incorrecta Responder Guardar contraseña en Keystore Error al restaurar base de datos Seleccionar contactos Guardar perfil de grupo - Restablecer colores + Reiniciar colores Sólo tú puedes enviar mensajes temporales. Sólo tu contacto puede enviar mensajes temporales. No se permiten mensajes de voz. @@ -606,8 +595,7 @@ llamada rechazada secreto Abrir SimpleX Chat para aceptar llamada - Restablecer valores por defecto - Guardar color + Reiniciar a valores predetarminados Pendiente Notificaciones periódicas Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes. @@ -616,8 +604,8 @@ Notificaciones privadas imagen del perfil No se permiten mensajes de voz. - Proteger la pantalla de la aplicación - repositorio GitHub .]]> + Proteger pantalla de la aplicación + repositorio GitHub .]]> Grabar mensaje de voz ha expulsado a %1$s Enviar previsualizacion de enlaces @@ -635,9 +623,8 @@ Rechazar Obligatorio Guardar y notificar contactos - Protocolo y código abiertos: cualquiera puede usar los servidores. + Cualquiera puede alojar servidores. Rol - Revertir Intervalo PING Contador PING Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar). (24 horas) @@ -653,7 +640,7 @@ Espacio reservado para la imagen del perfil Código QR Consultas y sugerencias - Dirección del servidor predefinida + Dirección predefinida del servidor Contacta vía email Valora la aplicación Guardar @@ -661,23 +648,21 @@ confirmación recibida… Periódico Privacidad redefinida - Saber más en nuestro repositorio GitHub. Rechazar Abrir Llamada pendiente Privacidad y Seguridad Guarda la contraseña de forma segura, NO podrás acceder al chat si la pierdes. - Guardar archivo 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. te ha expulsado Recibiendo vía - Tiempo de espera del protocolo + Timeout protocolo seg - Datos del perfil y conexiones + Eliminar perfil y conexiones No se permiten mensajes temporales. Sólo tú puedes enviar mensajes de voz. Sólo tu contacto puede enviar mensajes de voz. - EJECUTAR CHAT + EJECUTAR SIMPLEX Reinicia la aplicación para poder usar la base de datos importada. Introduce la contraseña actual correcta. recepción no permitida @@ -694,22 +679,21 @@ Abrir chat Restaurar copia de seguridad de la base de datos Guardar contraseña y abrir el chat - La contraseña no se ha encontrado en Keystore, introdúcela 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. + 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. Expulsar Expulsar miembro Enviar mensaje directo - Restablecer + Reiniciar Pegar Código de seguridad Escanea el código de seguridad desde la aplicación de tu contacto. Guardar servidores - Escanear código QR del servidor + Escanear código QR Servidor predefinido Guardar y notificar contacto ¿Guardar preferencias\? Guardar y notificar grupo - 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. + 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. 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. Bloqueo SimpleX Desbloquear @@ -731,7 +715,7 @@ envío no autorizado Escribe un nombre para el contacto Error desconocido - El rol del miembro cambiará a \"%s\" y se notificará al grupo. + El rol cambiará a %s. Todos serán notificados. La seguridad de SimpleX Chat ha sido auditada por Trail of Bits. Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido. Mensajes de chat SimpleX @@ -746,28 +730,27 @@ Compartir enlace de un uso ¿Actualizar el modo de aislamiento de transporte\? Altavoz activado - Para habilitar las acciones sobre la base de datos, debes parar Chat ¡La conexión que has aceptado se cancelará! - La base de datos no funciona correctamente. Pulsa para saber más + La base de datos no funciona correctamente. Pulsa para conocer más El mensaje será marcado como moderado para todos los miembros. - La nueva generación de mensajería privada + El futuro de la mensajería 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. Esta acción es irreversible. Los mensajes enviados y recibidos anteriores a la selección serán eliminados. Podría tardar varios minutos. Esta configuración se aplica a los mensajes del perfil actual ¡Esta cadena no es un enlace de conexión! - servicio en segundo planoSimpleX, usa un pequeño porcentaje de la batería al día.]]> + SimpleX se ejecuta en segundo plano en lugar de usar notificaciones push.]]> Configuración Altavoz desactivado Inciar chat nuevo - Para exportar, importar o eliminar la base de datos debes parar Chat. Mientra tanto no podrás recibir o enviar mensajes. - Gracias por instalar SimpleX Chat! - Para proteger tu 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. + Para exportar, importar o eliminar la base de datos debes parar SimpleX. Mientra tanto no podrás enviar ni recibir mensajes. + ¡Gracias por instalar SimpleX Chat! + Para proteger tu privacidad, SimpleX usa identificadores distintos para cada uno de tus contactos. Para proteger tu información, activa el Bloqueo SimpleX. \nSe te pedirá que completes la autenticación antes de activar esta función. - Al actualizar la configuración, el cliente se reconectará a todos los servidores. + Para actualizar la configuración el cliente se reconectará a todos los servidores. ¿Usar servidores SimpleX Chat\? Enlace de grupo SimpleX - Invitación única SimpleX + Invitación SimpleX de un uso Enlaces SimpleX El servidor requiere autorización para crear colas, comprueba la contraseña Para recibir notificaciones, introduce la contraseña de la base de datos @@ -776,7 +759,7 @@ Se muestran el nombre del contacto y el mensaje Se muestra sólo el nombre del contacto Bloqueo SimpleX activado - Parar Chat + Parar SimpleX El mensaje se eliminará para todos los miembros. Compartir archivo… ¡Demasiadas imágenes! @@ -785,7 +768,7 @@ Usar hosts .onion simplexmq: v%s (%2s) La plataforma de mensajería y aplicaciones que protege tu privacidad y seguridad. - La primera plataforma sin identificadores de usuario: diseñada para la privacidad. + Sin identificadores de usuario. Este grupo ya no existe. Establecer 1 día ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate. @@ -793,24 +776,23 @@ Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC. Aislamiento de transporte (para compartir con tu contacto) - Activar audio + Activar sonido %s está verificado %s no está verificado Probar servidor Probar servidores - Estrella en GitHub - Lista de servidores para las conexiones nuevas de tu perfil actual + Califica en GitHub + Servidores para conexiones nuevas en tu perfil ¿Usar conexión directa a Internet\? - ¿Actualizar la configuración de los hosts .onion\? El perfil sólo se comparte con tus contactos. inicializando… Mensajes omitidos CONFIGURACIÓN - ¿Parar Chat? + ¿Parar SimpleX? %s segundo(s) Pulsa para unirte ha actualizado el perfil del grupo - Tiempo de espera de la conexión TCP agotado + Timeout de la conexión TCP Tema Establece preferencias de grupo SOPORTE SIMPLEX CHAT @@ -819,46 +801,45 @@ Actualizar contraseña base de datos Pulsa para unirte en modo incógnito Cambiar - El rol del miembro cambiará a \"%s\" y recibirá una invitación nueva. + El rol cambiará a %s y el miembro recibirá una invitación nueva. Actualizar ¿Actualizar la configuración de red\? Intentando conectar con el servidor para recibir mensajes de este contacto. formato de mensaje desconocido Intentando conectar con el servidor para recibir mensajes de este contacto (error: %1$s). - Prueba fallida en el paso %s. + Prueba no superada en el paso %s. Pulsa para iniciar chat nuevo Compartir mensaje… Compartir medios… Mostrar - Error desconocido base de datos: %s + Error desconocido en la base de datos: %s El intento de cambiar la contraseña de la base de datos no se ha completado. Pulsa el botón Para iniciar un chat nuevo Cambiar servidor de recepción - Completamente descentralizado: sólo visible a los miembros. + Totalmente descentralizado. Visible sólo para los miembros. Para conectarte mediante enlace - ¡Error en prueba del servidor! - Algunos servidores no superaron la prueba: + ¡Prueba no superada! + Algunos servidores no han superado la prueba: Usar servidor - Usar para conexiones nuevas + Para conexiones nuevas Sistema mediante enlace de un solo uso Chats Mensaje de voz… Desactivar vídeo Activar vídeo - Contraseña de base de datos incorrecta + Contraseña incorrecta de la base de datos ¡Contraseña incorrecta! Te has unido a este grupo. Conectando con el emisor de la invitacíon. Estás usando un perfil incógnito en este grupo. Para evitar descubrir tu perfil principal no se permite invitar contactos Has sido invitado al grupo Mensajes de voz Tus contactos pueden permitir la eliminación completa de mensajes. - recibes los mensajes. Tus contactos controlan a través de qué servidor(es) envías tus mensajes.]]> Mensajes de voz - Los mensajes de voz no están permitidos en este grupo. + Mensajes de voz no permitidos. Comprobar la seguridad de la conexión - ¡Ya estás conectado a %1$s. + ¡Ya estás conectado con %1$s. ¡Bienvenido! Tu perfil será enviado \na tu contacto @@ -882,14 +863,14 @@ La base de datos actual será ELIMINADA y SUSTITUIDA por la importada. \nEsta acción es irreversible. Tu perfil, contactos, mensajes y archivos actuales se perderán. Tu perfil aleatorio - Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde. - Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde. - Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano. + Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde. + Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o revisa más tarde. + Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano. 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 Mediante navegador mediante %1$s Servicio SimpleX Chat - ¡Bienvenido %1$s ! + ¡Bienvenido %1$s! has sido invitado al grupo Esperando archivo Esperando imagen @@ -907,7 +888,7 @@ Comprobar código de seguridad Has aceptado la conexión Has invitado a tu contacto - Te conectarás al grupo cuando el dispositivo anfitrión esté en línea, por favor espera o compruébalo más tarde. + Te conectarás al grupo cuando el dispositivo anfitrión esté en línea, por favor espera o revisa más tarde. Configuración Servidores SMP ¡Tú controlas tu chat! @@ -956,7 +937,7 @@ Tu servidor Dirección de tu servidor Tu perfil actual - Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil. + Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil. Sistema Añadir mensaje de bienvenida Llamadas y videollamadas @@ -965,7 +946,7 @@ Guardar contraseña de perfil Contraseña para hacerlo visible Error al guardar contraseña de usuario - El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP. + El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP. El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada. Introduce la contraseña Ocultar @@ -976,7 +957,7 @@ Mensaje de bienvenida Mensaje de bienvenida ¡Hacer perfil privado! - No mostrar de nuevo + No volver a mostrar ¡Silenciado cuando está inactivo! Moderación de grupos Perfiles ocultos @@ -992,12 +973,12 @@ ¡Más mejoras en camino! Contraseña del perfil oculto ¿Guardar mensaje de bienvenida\? - Activar audio + Activar sonido Puedes ocultar o silenciar un perfil. Mantenlo pulsado para abrir el menú. Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. Ahora los administradores pueden: \n- eliminar mensajes de los miembros. -\n- desactivar el rol miembro (a rol \"observador\") +\n- desactivar el rol miembro (a rol "observador") Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda del menú Mis perfiles. Actualización de la base de datos Volviendo a versión anterior de la base de datos @@ -1011,10 +992,10 @@ Confirmar actualizaciones de la bases de datos 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 EXPERIMENTAL - IDs de la base de datos y opciónes de aislamiento de transporte. + IDs de la base de datos y opciones de aislamiento de transporte. El archivo se recibirá cuando el contacto termine de subirlo. La imagen se recibirá cuando el contacto termine de subirla. - Mostrar opciones de desarrollador + Mostrar opciones para desarrolladores Ocultar: Mostrar: Eliminar perfil @@ -1023,7 +1004,7 @@ Mostrar perfil Eliminar perfil Vídeo - El vídeo se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde. + El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. Esperando el vídeo Ha pedido recibir el video ¡Demasiados vídeos! @@ -1044,7 +1025,7 @@ Servidores XFTP Puerto puerto %d - Usar hosts .onion a No si el proxy SOCKS no los admite.]]> + Usar hosts .onion debe estar a No si el proxy SOCKS no los admite.]]> Descargar archivo Usar proxy SOCKS Host @@ -1115,17 +1096,17 @@ ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate. Vídeos y archivos de hasta 1Gb ¡Rápido y sin necesidad de esperar a que el remitente esté en línea! - Abrir perfiles + Cambiar perfil Más información - Si no puedes reunirte en persona, **muestra el código QR por videollamada**, o comparte el enlace. + Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace. Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación. Crear dirección SimpleX Auto aceptar Vista previa Abriendo base de datos… Error al introducir dirección - Guía de Usuario.]]> - Enlace un uso + Guía de Usuario.]]> + Enlace de un solo uso Dirección SimpleX Cuando alguien solicite conectarse podrás aceptar o rechazar su solicitud. Compartir dirección @@ -1162,7 +1143,7 @@ Mensaje enviado Dejar de compartir ¿Dejar de compartir la dirección\? - COLORES DEL TEMA + COLORES DE LA INTERFAZ Puedes crearla más tarde ¿Compartir la dirección con los contactos\? Compartir con contactos @@ -1224,7 +1205,7 @@ semanas Error al cargar detalles Los miembros pueden añadir reacciones a los mensajes. - Las reacciones a los mensajes no están permitidas en este grupo. + Reacciones a los mensajes no permitidas. Sólo tu contacto puede añadir reacciones a los mensajes. 1 minuto Registro actualiz @@ -1247,12 +1228,12 @@ Saber más Interfaz en japonés y portugués sin texto - Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat. + Han ocurrido algunos errores no críticos durante la importación: ¿Cerrar\? - Aplicación + APLICACIÓN Reiniciar Cerrar - Las notificaciones dejarán de funcionar hasta que reinicies la aplicación + Las notificaciones dejarán de funcionar hasta que vuelvas a iniciar la aplicación Desactivado Error al cancelar cambio de dirección Sin chats filtrados @@ -1264,12 +1245,12 @@ Cancelar cambio de dirección Archivos y multimedia No se permite el envío de archivos y multimedia. - Los archivos y multimedia no están permitidos en este grupo. - Los miembros del grupo pueden enviar archivos y multimedia. + Archivos y multimedia no permitidos. + Los miembros pueden enviar archivos y multimedia. Se permite enviar archivos y multimedia Favorito Sólo los propietarios del grupo pueden activar los archivos y multimedia. - Timeout de protocolo por KB + Timeout protocolo por KB renegociación de cifrado permitida para %s cifrado acordado cifrado ok @@ -1307,7 +1288,7 @@ Activar ¿Desactivar confirmaciones\? ¿Activar confirmaciones\? - Se pueden anular en la configuración de grupos y contactos. + Se puede modificar desde la configuración particular de cada grupo o contacto. Activar para todos Activar (conservar anulaciones) Desactivar para todos @@ -1352,9 +1333,9 @@ Conectar en incógnito Permitir Abrir configuración - Se compartirá un perfil nuevo aleatorio. + Compartirás un perfil nuevo aleatorio. Pega el enlace recibido para conectar con tu contacto… - Tu perfil %1$s será compartido. + El perfil %1$s será compartido. Desactivar notificaciones Sin llamadas en segundo plano. SimpleX no puede funcionar en segundo plano. Sólo recibirás notificaciones con la aplicación abierta. @@ -1372,7 +1353,7 @@ La contraseña aleatoria se almacenará en Configuración como texto plano. \nPuedes cambiarlo más tarde. La contraseña para el cifrado de la base de datos se actualizará y almacenará en Configuración - Eliminar contraseña de configuración\? + ¿Eliminar contraseña de configuración? Usar contraseña aleatoria Guardar contraseña en configuración Configuración contraseña base de datos @@ -1383,12 +1364,12 @@ Abrir Cifra archivos almacenados y multimedia Error al establecer contacto con el miembro - Recuerda: los servidores de retransmisión están conectados mediante SOCKS proxy. Las llamadas y las previsualizaciones de enlaces usan conexión directa.]]> - Cifra archivos locales + Recuerda: los servidores están conectados mediante proxy SOCKS, pero las llamadas y las previsualizaciones de enlaces usan conexión directa.]]> + Cifrar archivos locales Nueva aplicación para ordenador! - 6 idiomas nuevos para el interfaz + 6 nuevos idiomas para la interfaz Cifrado de los nuevos archivos locales (excepto vídeos). - Enviar mensaje directo para conectar + Envía un mensaje para conectar Descubre y únete a grupos Modo incógnito simplificado Árabe, Búlgaro, Finlandés, Hebreo, Tailandés y Ucraniano - gracias a los usuarios y Weblate. @@ -1432,7 +1413,7 @@ Renegociación de cifrado fallida. Ordenadores ¿Corregir el nombre a %s? - Elimina %d mensajes? + ¿Eliminar %d mensajes? Enlazar móvil ¿Conectar con %1$s? Bloquear @@ -1455,7 +1436,7 @@ Dirección ordenador incorrecta Dispositivo Ruta archivo no valida. - ¿Desconectar ordenador? + ¿Desconectar del ordenador? ¡Los mensajes nuevos de %s estarán ocultos! La versión de aplicación del ordenador %s no es compatible con esta aplicación. bloqueado @@ -1492,14 +1473,14 @@ Ordenador encontrado ¡No compatible! Esperando conexión móvil: - Eliminar miembro + Expulsar miembro ¿Desbloquear miembro? Para permitir que la aplicación móvil se conecte al ordenador, abre este puerto en el firewall si está habilitado Usar desde ordenador Código de sesión ¿Repetir solicitud de admisión? Crear perfil de chat - ¿Eliminar miembro? + ¿Expulsar miembro? ¡Ya estás conectando mediante este enlace de un solo uso! Desenlazar El nombre del dispositivo será compartido con el cliente móvil conectado. @@ -1510,7 +1491,7 @@ autor Pegar dirección de ordenador %1$s!]]> - Verificar código con ordenador + Verifica el código en el ordenador Escanear código QR desde ordenador Desbloquear Detectable mediante red local @@ -1525,35 +1506,35 @@ Verificar conexión Ningún móvil conectado Error aplicación - error mostrando contenido + error al mostrar el contenido error al mostrar mensaje Puedes hacerlo visible para tus contactos de SimpleX en Configuración. El historial no se envía a miembros nuevos. Reintentar Cámara no disponible - Enviar hasta 100 últimos mensajes a los miembros nuevos. + Se envían hasta 100 mensajes más recientes a los miembros nuevos. Añadir contacto: crea un enlace de invitación nuevo o usa un enlace recibido.]]> - No enviar historial a miembros nuevos. - O mostrar este código + No se envía el historial a los miembros nuevos. + O muestra el código QR Hasta 100 últimos mensajes son enviados a los miembros nuevos. - El código QR escaneado no es un enlace SimpleX. - El texto pegado no es un enlace SimpleX. + El código QR escaneado no es un enlace de SimpleX. + El texto pegado no es un enlace de SimpleX. Permitir acceso a la cámara Podrás ver el enlace de invitación en detalles de conexión. ¿Guardar invitación no usada? - Compartir este enlace de un uso + Comparte este enlace de un solo uso Crear grupo: crea un grupo nuevo.]]> Historial visible Código acceso app Nuevo chat Cargando chats… Creando enlace… - O escanear código QR + O escanea el código QR Código QR no válido Añadir contacto Pulsa para escanear Guardar - Pulsa para pegar enlace + Pulsa para pegar el enlace Buscar o pegar enlace SimpleX Con uso reducido de batería. bloqueado por administrador @@ -1561,7 +1542,7 @@ Error al crear mensaje Error al eliminar notas privadas ¿Eliminar notas privadas? - Opciones desarrollador + Opciones para desarrolladores ha bloqueado a %s ha desbloqueado a %s has bloqueado a %s @@ -1571,14 +1552,14 @@ ¿Bloqear miembro para todos? Creado: %s Bloquear para todos - ¿Desbloquear miembro para todos? + ¿Desbloquear el miembro para todos? Desbloquear para todos bloqueado bloqueado por administrador Error al bloqear el miembro para todos Con cifrado de archivos y multimedia. %s agotado]]> - Chat parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar Chat. + SimpleX está parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar SimpleX. Este nombre mostrado no es válido. Por favor, selecciona otro nombre. Conexión parada Conexión parada @@ -1589,15 +1570,14 @@ El ordenador está inactivo El ordenador está ocupado Error crítico - Todos los mensajes serán borrados. ¡No podrá deshacerse! + Todos los mensajes serán eliminados. ¡No puede deshacerse! ¿Iniciar chat? Mensaje de bienvenida demasiado largo Tiempo de espera para conectar con el ordenador agotado El ordenador tiene un código de invitación incorrecto El ordenador ha sido desconectado estado desconocido - Migración de la base de datos en progreso. -\nPodría tardar varios minutos. + Migración de la base de datos en curso. \nPodría tardar varios minutos. El ordenador tiene una versión sin soporte. Por favor, asegúrate de usar la misma versión en ambos dispositivos el contacto %1$s ha cambiado a %2$s perfil actualizado @@ -1632,9 +1612,9 @@ Miembro pasado %1$s el miembro %1$s ha cambiado a %2$s dirección de contacto eliminada - imagen de perfil eliminada + ha eliminado la imagen del perfil nueva dirección de contacto - nueva imagen de perfil + tiene nueva imagen del perfil Llamada Llamada finalizada Videollamada @@ -1691,7 +1671,7 @@ Finalizar migración Atención: el archivo será eliminado.]]> Comprueba tu conexión a internet y vuelve a intentarlo - Confirma que recuerdas la frase de contraseña de la base de datos para migrarla. + Para migrar la base de datos confirma que recuerdas la frase de contraseña. Error al verificar la frase de contraseña: Recuerda: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad.]]> Migrar desde otro dispositivo y escanea el código QR.]]> @@ -1702,9 +1682,9 @@ Error al exportar base de datos del chat Error al guardar ajustes El archivo exportado no existe - Para continuar, Chat debe estar parado. - cifrado de extremo a extremo con secreto perfecto hacía adelante, repudio y recuperación tras ataque.]]> - cifrado de extremo a extremo resistente a tecnología cuántica con secreto perfecto hacía adelante, repudio y recuperación tras ataque.]]> + Para continuar, SimpleX debe estar parado. + cifrado de extremo a extremo con secreto perfecto hacia adelante, repudio y recuperación tras ataque.]]> + cifrado de extremo a extremo resistente a tecnología cuántica con secreto perfecto hacia adelante, repudio y recuperación tras ataque.]]> Migrar aquí Migrar a otro dispositivo Migrar a otro dispositivo mediante código QR. @@ -1731,8 +1711,8 @@ Enlaces SimpleX no permitidos Mensajes de voz no permitidos Enlaces SimpleX - Los miembros del grupo pueden enviar enlaces SimpleX. - Los enlaces SimpleX no se permiten en este grupo. + Los miembros pueden enviar enlaces de SimpleX. + Enlaces SimpleX no permitidos. propietarios Móvil Sin conexión de red @@ -1740,10 +1720,10 @@ WiFi Ethernet por cable administradores - Activar para - No permitir el envío de enlaces SimpleX + Activado para + No se permite enviar enlaces SimpleX todos los miembros - Permitir enviar enlaces SimpleX. + Se permite enviar enlaces SimpleX. guardado guardado desde %s Guardado @@ -1767,7 +1747,644 @@ Al iniciar llamadas de audio y vídeo. ¡Será habilitado en los chats directos! Conexión de red más fiable. - Imágenes del perfil + Forma de los perfiles Dar forma a las imágenes de perfil Cuadrada, circular o cualquier forma intermedia. - \ No newline at end of file + Capacidad excedida - el destinatario no ha recibido los mensajes previos. + Error del servidor de destino: %1$s + Error: %1$s + Servidor de reenvío: %1$s +\nError del servidor de destino: %2$s + Servidor de reenvío: %1$s +\nError: %2$s + Problema en la red - el mensaje ha caducado tras muchos intentos de envío. + La versión del servidor es incompatible con la configuración de la red. + Enrutamiento privado + Servidores desconocidos + NO usar enrutamiento privado. + Enrutamiento de mensajes alternativo + Modo de enrutamiento de mensajes + No + Estado del mensaje + Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida. + Con IP oculta + Si + Enviar mensajes directamente cuando tu servidor o el de destino no admitan enrutamiento privado. + Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes. + NO enviar mensajes directamente incluso si tu servidor o el de destino no soportan enrutamiento privado. + Siempre + Permitir versión anterior + Usar siempre enrutamiento privado. + Aviso de entrega de mensaje + Nunca + ENRUTAMIENTO PRIVADO DE MENSAJES + La dirección del servidor es incompatible con la configuración de la red. + Con IP desprotegida + Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada + Usar enrutamiento privado con servidores de mensaje desconocidos. + Enviar mensajes directamente cuando tu dirección IP está protegida y tu servidor o el de destino no admitan enrutamiento privado. + ¡Servidores desconocidos! + Sin Tor o VPN, tu dirección IP será visible para estos relés XFTP: +\n%1$s. + Proteger dirección IP + Sin Tor o VPN, tu dirección IP será visible para los servidores de archivos. + ARCHIVOS + La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion o cuando esté habilitado el proxy SOCKS). + Colores del chat + Tema del chat + Aplicar a + Negro + Todos los modos + Modo de color + Oscuro + Modo oscuro + Colores en modo oscuro + Relleno + ¡Buenas tardes! + ¡Buenos días! + Claro + Modo claro + Respuesta recibida + Mosaico + Quitar imagen + Reiniciar color + Escala + Respuesta enviada + Establecer tema predefinido + Sistema + Color de fondo + Encaje + Color adicional 2 + Configuración avanzada + Tema del perfil + Listado del chat en ventana nueva + Color imagen de fondo + información cola del servidor: %1$s +\n +\núltimo mensaje recibido: %2$s + Reiniciar al tema de la aplicación + Enrutamiento privado de mensajes 🚀 + Recibe archivos de forma segura + Mejora del envío de mensajes + Con uso reducido de la batería. + Tema de la app + Confirma archivos de servidores desconocidos. + Informe debug + ¡Cambia el aspecto de tus chats! + Nuevos temas de chat + Información cola de mensajes + ninguno + Protege tu dirección IP de los servidores elegidos por tus contactos.\nActívalo en *Servidores y Red*. + Reiniciar al tema del usuario + Error al inicializar WebView. Actualiza tu sistema a la última versión. Por favor, ponte en contacto con los desarrolladores. +\nError: %s + Interfaz en persa + Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado. + Archivo no encontrado, probablemente haya sido eliminado o cancelado. + Error del servidor de archivos: %1$s + Error de archivo + Error en archivo temporal + Estado del mensaje: %s + Estado del mensaje + Estado del archivo: %s + Estado del archivo + 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. + Copiar error + Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador. + No se puede enviar el mensaje + Las preferencias seleccionadas no permiten este mensaje. + Estadísticas servidores + Archivos + Mostrando + Suscritas + Errores de suscripción + Suscripciones ignoradas + Para ser notificado sobre versiones nuevas, activa el chequeo periódico para las versiones Estable o Beta. + Beta + Servidores SMP configurados + Servidores XFTP configurados + Servidores conectados + Conectando + Perfil actual + Zoom + Subido + Actualización disponible: %s + Descarga de actualización cancelada + Actualización descargada + Buscar actualizaciones + Buscar actualizaciones + Estadísticas + Total + Sesiones de transporte + Servidor XFTP + No tienes conexión directa a estos servidores. Los mensajes destinados a estos usan enrutamiento privado. + Todos los perfiles + Conectadas + Estadísticas detalladas + Detalles + Archivos subidos + Errores de subida + intentos + Completadas + Conexiones + Creadas + errores de descifrado + Eliminadas + Errores de eliminación + inactivo + Mensaje reenviado + El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo. + Miembro inactivo + Por favor, inténtalo más tarde. + Error de enrutamiento privado + La dirección del servidor es incompatible con la configuración de red: %1$s. + La versión del servidor es incompatible con tu aplicación: %1$s. + Tamaño de la fuente + Error al reiniciar las estadísticas + Reiniciar + Las estadísticas de los servidores serán restablecidas. ¡No puede deshacerse! + Descargado + Servidor SMP + Aún no hay conexión directa, el mensaje es reenviado por el administrador. + Otros servidores SMP + Otros servidores XFTP + Escanear / Pegar enlace + Mostrar porcentaje + Desactivar + Desactivado + Descargando actualización, por favor no cierres la aplicación + Descarga %s (%s) + Instalación completada + Instalar actualización + Abrir ubicación del archivo + Por favor, reinicia la aplicación. + Recordar más tarde + Omitir esta versión + Estable + inactivo + Error + Error al reconectar con el servidor + Error al reconectar con los servidores + Errores + Recepción de mensajes + Mensajes recibidos + Mensajes enviados + Sin información, intenta recargar + Pendientes + Servidores conectados previamente + Servidores con proxy + Mensajes recibidos + Total recibidos + Errores de recepción + Reconectar todos los servidores + ¿Reconectar servidor? + ¿Reconectar servidores? + Reconectar con el servidor para forzar la entrega de mensajes. Se usa tráfico adicional. + Reconectar todos los servidores para forzar la entrega de mensajes. Usa tráfico adicional. + Reiniciar estadísticas + ¿Reiniciar todas las estadísticas? + Mensajes enviados + Total enviados + Archivos descargados + Errores de descarga + duplicados + caducados + Abrir configuración del servidor + otros + otros errores + Como proxy + Reconectar + Aseguradas + Errores de envío + Directamente + Mediante proxy + Dirección del servidor + Tamaño + Conexiones activas + Iniciado el %s. + Iniciado el %s \nLos datos son privados en tu dispositivo. + Bloques eliminados + Bloques descargados + Bloques subidos + Confirmaciones + Errores de confirmación + Conectando con el contacto, por favor espera o revisa más tarde. + Estado de tu conexión y servidores. + Conecta más rápido con tus amigos + Controla tu red + Protege tu dirección IP y tus conexiones. + ¿Permitir llamadas? + Difuminar multimedia + ¡Llamadas no permitidas! + Base de datos exportada + Guardar y reconectar + Reiniciar todas las pistas + Pegar enlace + Medio + Suave + Barra de herramientas accesible + llamar + conectar + ¿Eliminar %d mensajes de miembros? + mensaje + Mensaje + Nada seleccionado + Los mensajes se marcarán para eliminar. El destinatario o destinatarios podrán revelar estos mensajes. + abrir + Seleccionar + Seleccionados %d + Configuración + ¿Confirmas la eliminación del contacto? + ¡Contacto eliminado! + El contacto será eliminado. ¡No puede deshacerse! + ¡Conversación eliminada! + Elimina sin notificar + Eliminar sólo la conversación + Conservar conversación + buscar + Contactos archivados + Servidores de mensajes + Servidores de archivos y multimedia + Proxy SOCKS + No + Continuar + Algunos archivos no han sido exportados + No se puede llamar al contacto + No se puede llamar al miembro del grupo + No se pueden enviar mensajes al miembro del grupo + El contacto está eliminado. + Invitar + Por favor, pide a tu contacto que active las llamadas. + Enviar mensaje para activar llamadas. + Elimina hasta 20 mensajes a la vez. + Barra de menú accesible + Archiva contactos para charlar más tarde. + Puedes guardar el archivo exportado. + Fuerte + Los mensajes serán eliminados para todos los miembros. + Los mensajes serán marcados como moderados para todos los miembros. + video + Puedes enviar mensajes a %1$s desde Contactos archivados + Aún puedes ver la conversación con %1$s en la lista de chats. + Puedes migrar la base de datos exportada. + Conexión TCP + Usa la aplicación con una sola mano. + La dirección del servidor de destino de %1$s es incompatible con la configuración del servidor de reenvío %2$s. + La versión del servidor de destino de %1$s es incompatible con el servidor de reenvío %2$s. + Tus contactos + Debes permitir que tus contacto te llamen para poder llamarles. + Error al conectar con el servidor de reenvío %1$s. Por favor, inténtalo más tarde. + La dirección del servidor de reenvío es incompatible con la configuración de red: %1$s. + El servidor de reenvío %1$s no ha podido conectarse al servidor de destino %2$s. Por favor, intentalo más tarde. + La versión del servidor de reenvío es incompatible con la configuración de red: %1$s. + Ningún contacto filtrado + Difumina para mayor privacidad + Crear + Alternar lista de chats: + Ajusta el tamaño de la fuente. + Reproduce desde la lista de chats. + Actualizar la aplicación automáticamente + Invitar + Nueva experiencia de chat 🎉 + Nuevas opciones multimedia + Puedes cambiar la posición de la barra desde el menú Apariencia. + Descarga nuevas versiones desde GitHub. + Mensaje nuevo + Enlace no válido + Por favor, comprueba que el enlace SimpleX es correcto. + %1$d archivo(s) se está(n) descargando todavía. + %1$d otro(s) error(es) de archivo. + BASE DE DATOS + Error en reenvío de mensajes + ¿Reenviar %1$s mensaje(s)? + Reenviar mensajes… + Modo de sistema + ¿Reenviar mensajes sin los archivos? + Asegúrate de que la configuración del proxy es correcta. + %1$d archivo(s) ha(n) sido eliminado(s). + %1$d error(es) de archivo:\n%2$s + La descarga ha fallado para %1$d archivo(s). + %1$d archivo(s) no se ha(n) descargado. + %1$s mensajes no enviados + Descargar + Reenviando %1$s mensajes + Los mensajes han sido eliminados después de seleccionarlos. + ¡Nada para reenviar! + Guardando %1$s mensajes + No uses credenciales con proxy. + Error guardando proxy + Contraseña + Autenticación proxy + Se usan credenciales proxy diferentes para cada conexión. + Se usan credenciales proxy diferentes para cada perfil. + Credenciales aleatorias + Nombre de usuario + Tus credenciales podrían ser enviadas sin cifrar. + ¿Eliminar archivo? + El archivo de bases de datos subido será eliminado permanentemente de los servidores. + Los mensajes serán eliminados. ¡No puede deshacerse! + Error al cambiar perfil + Selecciona perfil de chat + Perfil a compartir + Tu conexión ha sido trasladada a %s pero ha ocurrido un error inesperado al redirigirte al perfil. + Sonido silenciado + Error al iniciar WebView. Asegúrate de tener WebView instalado y que sea compatible con la arquitectura amr64.\nError: %s + Forma del mensaje + Esquinas + Cola + Se usarán credenciales SOCKS nuevas cada vez que inicies la aplicación. + por sesión + Abre la configuración de Safari / Sitios Web / Micrófono y a continuación selecciona Permitir para localhost. + Pulsa el botón info del campo dirección para permitir el uso del micrófono. + Para hacer llamadas, permite el uso del micrófono. Cuelga e intenta llamar de nuevo. + Se usarán credenciales SOCKS nuevas para cada servidor. + por servidor + Llamadas mejoradas + Sistema de fechas mejorado. + Experiencia de usuario mejorada + Forma personalizable de los mensajes. + Desplazamiento de hasta 20 mensajes. + Protocolos de SimpleX auditados por Trail of Bits. + Intercambia audio y video durante la llamada. + Seguridad mejorada ✅ + Elimina o modera hasta 200 mensajes a la vez. + Cambia el perfil de chat para invitaciones de un solo uso. + Error al guardar servidores + Error en la configuración del servidor. + Para el perfil de chat %s: + Sin servidores para mensajes. + Sin servidores para recibir archivos. + Sin servidores para enviar archivos. + Seguridad de conexión + Compartir enlace de un uso con un amigo + Comparte tu dirección SimpleX en redes sociales. + Ajustes de dirección + Crear enlace de un uso + Para redes sociales + ¿Dirección SimpleX o enlace de un uso? + Operadores de servidores + Operadores de red + Las condiciones de los operadores habilitados serán aceptadas después de 30 días. + Revisar más tarde + Condiciones aceptadas el: %s. + Operador de red + Operador + Servidores predefinidos + Revisar condiciones + %s servidores + Las condiciones serán aceptadas el: %s. + Condiciones de uso + Para enrutamiento privado + Error al añadir servidor + Abrir cambios + Abrir condiciones + Servidor añadido al operador %s. + El operador del servidor ha cambiado. + El protocolo del servidor ha cambiado. + Barras de herramientas + Difuminar + Navegación en el chat mejorada + Descentralización de la red + - El chat abre en el primer mensaje no leído.\n- Desplazamiento hasta los mensajes citados. + Aceptar condiciones + Condiciones aceptadas + Servidores de archivos y multimedia añadidos + Servidores de mensajes añadidos + ¿Dirección o enlace de un solo uso? + Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %s. + Continuar + El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace: + Activa Flux en Servidores y Red para mejorar la privacidad de los metadatos. + Error al aceptar las condiciones + Error al actualizar el servidor + para mejorar la privacidad de los metadatos. + Ningún mensaje + Servidor nuevo + Sin servidores para archivos y multimedia. + Sin servidores para enrutamiento privado. + Sin servidores para recibir mensajes. + Servidor del operador + O para compartir en privado + Selecciona los operadores de red a utilizar + Compartir dirección públicamente + Compartir enlaces de un solo uso y direcciones SimpleX es seguro a través de cualquier medio. + Actualizar + Sitio web + Tus servidores + Usar %s + Usar servidores + Uso para mensajes + Ver condiciones + Para recibir + Para enviar + Uso para archivos + Transparencia + Ver condiciones actualizadas + Mensajes no entregados + solamente con un contacto - comparte en persona o mediante cualquier aplicación de mensajería.]]> + Puedes añadir un nombre a la conexión para recordar a quién corresponde. + La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación. + Puedes configurar los operadores desde los ajustes de Servidores y Red. + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s, debes aceptar las condiciones de uso.]]> + Servidores para enviar archivos en tu perfil + ¡Segundo operador predefinido! + Puedes configurar los servidores a través de su configuración. + Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto. + %s.]]> + %s.]]> + 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. + Pulsa Crear dirección SimpleX en el menú para crearla más tarde. + La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado. + El mensaje ha sido eliminado o aún no se ha recibido. + Móvil remoto + O importa desde un archivo + Mensajes directos entre miembros de este chat no permitidos. + En dispositivos Xiaomi: por favor, habilita el Autoinicio en los ajustes del sistema para que las notificaciones funcionen.]]> + Por favor, reduce el tamaño del mensaje y envíalo de nuevo. + Por favor, reduce el tamaño del mensaje o elimina los archivos y envíalo de nuevo. + Puedes copiar y reducir el tamaño del mensaje para enviarlo. + Añade a miembros de tu equipo a las conversaciones. + Notificaciones y batería + Invitar al chat + Añadir amigos + Añadir miembros del equipo + El chat será eliminado para todos los miembros. ¡No puede deshacerse! + Eliminar chat + ¿Eliminar chat? + Salir del chat + El chat será eliminado para tí. ¡No puede deshacerse! + Sólo los propietarios del chat pueden cambiar las preferencias. + El miembro será eliminado del chat. ¡No puede deshacerse! + El rol cambiará a %s. Todos serán notificados. + Dejarás de recibir mensajes de este chat. El historial del chat se conserva. + Cómo ayuda a la privacidad + Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién. + Tu perfil de chat será enviado a los miembros de chat + Chats empresariales + ¿Salir del chat? + Privacidad para tus clientes. + invitación aceptada + solicitado para conectar + Dirección empresarial + Comprobar mensajes cada 10 min. + Sin servicio en segundo plano + Chat + Barra de chat accesible + Mensajes directos entre miembros no permitidos. + %1$s.]]> + ¡El chat ya existe! + Acerca de los operadores + La aplicación siempre funciona en segundo plano + cifrados de extremo a extremo y con seguridad postcuántica en mensajes directos.]]> + ¡Mensaje demasiado largo! + Simplex Chat y Flux han acordado incluir en la aplicación servidores operados por Flux. + Activar registros + Error al guardar base de datos + La conexión requiere renegociar el cifrado. + Renegociación de cifrado en curso. + Reparar + ¿Reparar conexión? + Conexión no establecida. + Error al cargar listas de chat + Error al actualizar las listas de chat + Favoritos + Lista + Sin chats + Ningún chat encontrado + Sin chats en la lista %s. + Añadir lista + Todo + Empresas + Contactos + Grupos + Abrir con %s + Añadir a la lista + Todos los chats se quitarán de la lista %s y esta será eliminada + Crear lista + Eliminar + ¿Eliminar lista? + Editar + Nombre de la lista... + El nombre y el emoji deben ser diferentes en todas las listas. + Guardar lista + Ningún chat sin leer + Error al crear lista de chat + Notas + Reordenar + Cambiar lista + Error al crear informe + Error al guardar la configuración + informe archivado por %s + ¿Archivar informe? + El informe será archivado para ti. + Informar + Eliminar informe + 1 informe + Informes + Informar de spam: sólo los moderadores del grupo lo verán. + Informar de violación: sólo los moderadores del grupo lo verán. + Informar de contenido: sólo los moderadores del grupo lo verán. + Informar de otros: sólo los moderadores del grupo lo verán. + moderador + El contenido viola las condiciones de uso. + Spam + Conexión bloqueada + Conexión bloqueada por el operador del servidor:\n%1$s. + ¿Motivo del informe? + Archivo bloqueado por el operador del servidor:\n%1$s + Archivar + Archivar informe + Informes de miembros + %d informes + Informar del perfil de un miembro: sólo los moderadores del grupo lo verán. + Otro motivo + informes archivados + Violación de las normas de la comunidad + Contenido inapropiado + Perfil inapropiado + Solo el remitente y el moderador pueden verlo + Solo tú y los moderadores podéis verlo + Spam + Abrir enlaces desde listado de chats + Si + No + ¿Abrir enlace web? + Preguntar + Abrir enlace + Nombre para el chat… + ¿Desactivar la eliminación automática de mensajes? + Desactivar + ¿Modificar la eliminación automática de mensajes? + Elimina los mensajes del dispositivo + Los mensajes de esta conversación nunca se eliminan. + Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No puede deshacerse! + 1 año + predefinido (%s) + Puerto TCP para mensajes + Se usa el puerto TCP %1$s cuando no se ha especificado otro. + Usar puerto web + Silenciar todo + Menciones sin leer + ¡Puedes mencionar hasta %1$s miembros por mensaje! + Los miembros pueden informar de mensajes a los moderadores. + No se permite informar de mensajes a los moderadores. + Se permite informar de mensajes a los moderadores. + Informe: %s + ¿Archivar %d informes? + Archivar informes + Para todos los moderadores + para mí + No se permite informar de mensajes en este grupo. + Todos los informes serán archivados para ti. + ¿Archivar todos los informes? + Las menciones ahora se notifican. + Menciona a miembros 👋 + No pierdas los mensajes importantes. + Organiza tus chats en listas + Nombres privados en archivos de media. + Eliminación más rápida de grupos. + Envío más rápido de mensajes. + Ayuda a los admins a moderar sus grupos. + Rendimiento de grupos mejorado + Privacidad y seguridad mejoradas + Envía informes privados + Establece el vencimiento para los mensajes en los chats. + rechazado + rechazado + ¿Expulsar miembros? + ¡Los mensajes de estos miembros serán mostrados! + ¿Desbloquear los miembros para todos? + ¡Todos los mensajes nuevos de estos miembros estarán ocultos! + ¿Bloquear miembros para todos? + Los miembros serán expulsados del chat. ¡No puede deshacerse! + Condiciones actualizadas + moderadores + Los miembros serán expulsados del grupo. ¡No puede deshacerse! + pendiente de aprobación + pendiente + Error al leer la frase de contraseña de la base de datos + 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. + La frase de contraseña no se ha podido leer en Keystore. Por favor, introdúcela manualmente. Puede deberse a alguna actualización del sistema incompatible con la aplicación. Si no es así, por favor, ponte en contacto con los desarrolladores. + Aceptar + Política de privacidad y condiciones de uso. + Los chats privados, los grupos y tus contactos no son accesibles para los operadores de servidores. + Al usar SimpleX Chat, aceptas:\n- enviar únicamente contenido legal en los grupos públicos.\n- respetar a los demás usuarios - spam prohibido. + Configurar operadores de servidores + Enlace de canal SimpleX + Enlace completo + Enlace corto + 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. + Enlace de conexión no compatible + Usar enlaces cortos (BETA) + Usar puerto TCP 443 solo en servidores predefinidos. + Todos los servidores + Servidores predefinidos + No + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 8ef94b53fe..b8e99587f7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -5,8 +5,8 @@ %1$d پیام از قلم افتاده %1$s عضو %1$s می‌خواهد به شما متصل شود، به وسیله - 1 روز - 1 دقیقه + ۱ روز + ۱ دقیقه لغو لغو تغییر نشانی تغییر نشانی را لغو می‌کنید؟ @@ -36,7 +36,7 @@ مسیر نامعتبر پرونده برنامه از کار افتاد در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s). - حذف شده + حذف شد علامت گذاشته شده به عنوان حذف شده توسط %s حذف شد مسدود @@ -49,15 +49,15 @@ داده نامعتبر خطا در نمایش محتوا خطا در رمزگشایی - 5 دقیقه + ۵ دقیقه درباره نشانی سیمپل‌اکس(SimpleX) لینک یک بار مصرف درباره سیمپل‌اکس چت(SimpleX Chat) %1$d پیام از قلم افتاد. - 1 ماه - 1 هفته - 6 زبان جدید برای رابط کاربری - 30 ثانیه + ۱ ماه + ۱ هفته + ۶ زبان جدید برای رابط کاربری + ۳۰ ثانیه شما لینک یک بار مصرف ناشناس به اشتراک گذاشتید به وسیله لینک گروه ناشناس به وسیله لینک گروه @@ -229,25 +229,25 @@ توقف دریافت پرونده متوقف خواهد شد. خوش آمدید! - خطا در رمزگشایی + خطا در کدبرداری ارسال پیام مستقیم برای اتصال لطفا، تا زمانی که پرونده در حال بارگیری از موبایل متصل است، منتظر باشید. حذف مخاطب - مشاهده رمز امنیتی - تایید رمز امنیتی + مشاهده کد امنیتی + تایید کد امنیتی ارسال پیام ناپدید شونده لینک دعوت یک‌بارمصرف ایجاد گروه محرمانه (برای اشتراک‌گذاری با مخاطبتان) اگر لینک دعوت SimpleX Chat دریافت کردید، می‌توانید آن را در مرورگر خود باز کنید: - ورود رمز عبور + ورود کد عبور %d ثانیه - رمز عبور فعلی + کد عبور فعلی کپی پیام به عنوان حذف شده علامت‌گذاری خواهد شد. گیرنده‌ها قادر خواهند بود این پیام را آشکار کنند. ارسال غیرموفق برای اتصال لمس کنید - امکان رمزگشایی ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. + امکان کدبرداری ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. تصویر پرونده پیدا نشد لمس دکمه @@ -278,10 +278,10 @@ تماس تصویری برای محافظت از اطلاعاتتان، قفل SimpleX را روشن کنید. \nاز شما خواسته خواهد شد قبل از فعال شدن این ویژگی، تصدیق را تکمیل کنید. - عدم وجود رمز عبور + عدم وجود کد عبور %d دقیقه فعال‌سازی قفل SimpleX - وارد کردن رمز عبور + وارد کردن کد عبور ذخیره شده فرستاده شده فرستاده شده از @@ -323,7 +323,7 @@ مخاطب و تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! پیام‌های صوتی مجازند؟ پیام‌های صوتی ممنوع هستند! - اسکن رمز QR.]]> + اسکن کد QR.]]> اشتراک‌گذاری پرونده… تعداد تصویر بیش از اندازه! برای اسکن لمس کنید @@ -376,7 +376,7 @@ در انتظار تصویر پیام صوتی… تعیین نام مخاطب… - قطع اتصال + قطع شد در حال انتظار نشانی‌ دریافت تغییر کند؟ تغییر نشانی‌ لغو خواهد شد. نشانی‌ دریافت پیشین استفاده خواهد شد. @@ -384,7 +384,7 @@ فقط صاحبان گروه می‌توانند پیام‌های صوتی را فعال کنند. تایید بازنشاندن - اسکن رمز QR + اسکن کد QR (اسکن یا الصاق از حافظه) (تنها ذخیره شده توسط اعضای گروه) فعال کردن دسترسی دوربین @@ -404,7 +404,7 @@ ضبط پیام صوتی ارسال پیام بدون جزئیات - اتصال به وسیله لینک / رمز QR + اتصال به وسیله لینک / کد QR دوربین موجود نیست تصویر ایجاد لینک دعوت یک‌بارمصرف @@ -413,14 +413,14 @@ اتصال از طریق لینک تصویر وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید! نوع قفل SimpleX - تصدیق سامانه - تغییر رمز عبور + تصدیق سیستم + تغییر کد عبور در پاسخ به فوری تصدیق دستگاه فعال نیست. زمانی که تصدیق دستگاه را فعال کنید می‌توانید قفل SimpleX را از طریق تنظیمات روشن کنید. اجازه دادن ضمیمه - امکان رمزگشایی تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. + امکان کدبرداری تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. پرونده‌ها و رسانه ممنوع است! تصویر ارسال شد در انتظار ویدئو @@ -470,7 +470,7 @@ راهنمای مارکداون لینک نامعتبر! اشتراک‌گذاری لینک یک بار مصرف - یا رمز QR را اسکن کنید + یا کد QR را اسکن کنید تلاش مجدد عبارت عبور و صدور پایگاه داده افزودن سرورهای از پیش تنظیم شده @@ -483,7 +483,7 @@ SimpleX Chat را برای ترمینال نصب کنید در GitHub ستاره بزنید همکاری کنید - رمز QR + کد QR یک نمایه تصادفی جدید به اشتراک گذاشته خواهد شد. اتصال از طریق لینک لینک دعوت یک‌بارمصرف @@ -506,11 +506,11 @@ وقتی دستگاه میزبان گروه آنلاین شد، به گروه متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید! لینکی که دریافت کردید را الصاق کنید تا به مخاطبتان متصل شوید… نمایه شما %1$s به اشتراک گذاشته خواهد شد. - برای اتصال، مخاطبتان می‌تواند رمز QR را اسکن یا از لینک در برنامه استفاده کند. - اگر نمی‌توانید ملاقات حضوری داشته باشید، رمز QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید. - می‌توانید نشانی‌ خود را به صورت لینک یا رمز QR به اشتراک بگذارید - هر کسی می‌تواند به شما متصل شود. + برای اتصال، مخاطبتان می‌تواند کد QR را اسکن یا از لینک در برنامه استفاده کند. + اگر نمی‌توانید ملاقات حضوری داشته باشید، کد QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید. + می‌توانید نشانی خود را به صورت لینک یا کد QR به اشتراک بگذارید - هر کسی می‌تواند به شما متصل شود. وقتی اشخاص درخواست اتصال کنند، شما می‌توانید آن را بپذیرید یا رد کنید. - یا این رمز را نشان دهید + یا این کد را نشان دهید می‌توانید دوباره لینک دعوت را در جزئیات اتصال مشاهده کنید. نگه‌داشتن در حال ایجاد لینک… @@ -520,31 +520,31 @@ سرور از پیش تنظیم شده نشانی‌ سرور نامعتبر! به‌کارگیری برای اتصال‌های جدید - رمز QR نامعتبر - رمز امنیتی نادرست! - رمز امنیتی را از برنامه مخاطبتان اسکن کنید. + کد QR نامعتبر + کد امنیتی نادرست! + کد امنیتی را از برنامه مخاطبتان اسکن کنید. علامت‌گذاری به عنوان تایید شده %s تایید نشده است برای تایید رمزگذاری سرتاسر، روی دستگاه‌های خود، کد را با مخاطبتان مقایسه(یا اسکن) کنید. سرورهای XFTP شما - چگونه + روش استفاده در حال استفاده از سرورهای SimpleX Chat. تنظیم سرورهای ICE می‌خواهد به شما متصل شود! وقتی دستگاه مخاطبتان آنلاین شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید! - رمز QR را در تماس تصویری اسکن کنید، یا مخاطبتان می‌تواند یک لینک دعوت به اشتراک بگذارد.]]> + کد QR را در تماس تصویری اسکن کنید، یا مخاطبتان می‌تواند یک لینک دعوت به اشتراک بگذارد.]]> دعوت استعمال نشده نگه داشته شود؟ نشانی‌ SimpleX شما مارکداون در پیام‌ها ایده‌ها و سوالات را ارسال کنید به ما ایمیل بفرستید قفل SimpleX - افزودن سرور… + افزودن سرور آزمایش سرورها ذخیره سرورها عدم موفقیت آزمایش سرور! عدم موفقیت آزمایش چند سرور: - اسکن رمز QR سرور + اسکن کد QR سرور سرورهای SMP شما سرورهای XFTP از سرورهای SimpleX Chat استفاده شود؟ @@ -557,7 +557,7 @@ نشانی‌ SimpleX پاک‌سازی تایید نشانی‌ سرور از پیش تنظیم شده - رمز امنیتی + کد امنیتی به‌کارگیری از سرور سرورها برای اتصال‌های جدید نمایه گپ فعلی شما سرورها ذخیره شوند؟ @@ -567,17 +567,17 @@ لازم است مخاطبتان آنلاین باشد تا اتصال کامل شود. \nمی‌توانید این اتصال را لغو و مخاطب را حذف کنید (و بعدا با یک لینک جدید امتحان کنید). نشانی‌ SimpleX - این رمز QR یک لینک نیست! + این کد QR یک لینک نیست! راهنمای کاربر.]]> این رشته متن، یک لینک اتصال نیست! - رمزی که اسکن کردید یک رمز QR لینک SimpleX نیست. - اسکن رمز + کدی که اسکن کردید یک کد QR لینک SimpleX نیست. + اسکن کد تصویر نمایه بیشتر - نمایش رمز QR - رمز QR نامعتبر + نمایش کد QR + کد QR نامعتبر وقتی درخواست اتصال شما پذیرفته شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید! - رمز QR را در تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.]]> + کد QR را در تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.]]> نمایه گپ شما ارسال خواهد شد \nبه مخاطبتان اطلاعات بیشتر @@ -588,7 +588,6 @@ میزبان‌های Onion برای اتصال الزامی خواهد بود. \nلطفا توجه داشته باشید: شما بدون نشانی‌ onion. قادر نخواهید بود به سرورها متصل شوید. ذخیره - تنظیمات میزبان‌های onion. به روز شود؟ پورت از پروکسی SOCKS استفاده شود؟ از اتصال مستقیم اینترنت استفاده شود؟ @@ -597,7 +596,6 @@ هاست الزامی از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد. - از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد. ظاهر نسخه برنامه: v%s نمایش گزینه‌های توسعه‌دهنده @@ -608,12 +606,11 @@ برای هر نمایه گپی که در برنامه دارید استفاده خواهد شد.]]> نمایش خطاهای داخلی نمایش تماس‌های کند API - از میزبان‌های Onion استفاده نخواهد شد. خیر استفاده از میزبان‌های onion. را روی «خیر» تنظیم کنید اگر پروکسی SOCKS از آنها پشتیبانی نمی‌کند.]]> سفارشی کردن تم نسخه برنامه - رنگ‌های تم + رنگ‌های رابط کاربری نسخه هسته: v%s simplexmq: v%s (%2s) اعلان‌ها از کار خواهند افتاد تا زمانی که برنامه را دوباره راه‌اندازی کنید @@ -630,7 +627,6 @@ اتصال ساختار برنامه: %s تنظیمات شبکه - میزبان‌های Onion برای اتصال الزامی خواهد بود. نمایش: لطفا توجه داشته باشید: واسطه‌های پیام و پرونده از طریق پروکسی SOCKS متصل می‌شوند. تماس‌ها و ارسال پیش‌نمایش‌های لینک از اتصال مستقیم استفاده می‌کنند.]]> گزینه‌های توسعه‌دهنده @@ -640,8 +636,7 @@ تمام مخاطبانتان متصل باقی خواهند ماند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد. اگر تایید کنید، سرورهای پیام‌رسانی خواهند توانست نشانی‌ IP، و فراهم‌کننده شما را ببینند - و این که به چه سرورهایی متصل می‌شوید. مطمئن شوید قالب نشانی‌های سرور WebRTC ICE صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند. - یک اتصال جدای TCP (و اطلاعات ورود SOCKS) برای هر مخاطب و عضو گروه استفاده خواهد شد. -\nلطفا توجه داشته باشید: اگر اتصال‌های زیادی داشته باشید، مصرف باتری و ترافیک شما می‌تواند به شکل قابل توجه بالاتر باشد و بعضی اتصال‌ها ممکن است با موفقیت انجام نشوند. + برای هر مخاطب و عضو گروه استفاده خواهد شد. \nلطفا توجه داشته باشید: اگر اتصال‌های زیادی داشته باشید، مصرف باتری و ترافیک شما می‌تواند به شکل قابل توجه بالاتر باشد و بعضی اتصال‌ها ممکن است با موفقیت انجام نشوند.]]> حالت انزوای ترابری به روز شود؟ ویرایش تصویر ایجاد نشانی‌ SimpleX @@ -682,4 +677,1178 @@ یک نشانی ایجاد کنید تا اشخاص بتوانند به شما متصل شوند. نمایه شما روی دستگاهتان ذخیره شده و فقط با مخاطبانتان به اشتراک گذاشته می‌شود. سرورهای SimpleX قادر به دیدن نمایه شما نیستند. خطا در ذخیره کردن کلمه عبور کاربر - \ No newline at end of file + تماس‌های صوتی و تصویری + تماس‌های شما + همیشه از واسطه استفاده شود + همتا به همتا + متناوب + تماس در حال انتظار + تماس ناموفق + تماس پایان یافت + پاسخ به تماس + فوری + از طریق واسطه + صدا روشن + چرخش دوربین + پیش‌نویس پیام + وقتی برنامه در حال اجراست + تماس صوتی رمزگذاری سرتاسر شده + پذیرفتن + حریم خصوصی شما + حالت قفل + ارسال + کد عبور نادرست + کد عبور + مخاطبان + غیرفعال کردن (نگه‌داشتن مقدارهای جایگزین شده) + فعال کردن (نگه‌داشتن مقدارهای جایگزین شده گروه) + غیرفعال برای همه گروه‌ها + کمک + گپ‌ها + اجرای گپ + گپ در حال اجراست + توقف + حذف تمام پرونده‌ها + اصلاح نام به %s؟ + بعدا از طریق تنظیمات قابل تغییر است. + تماس پذیرفته نشده + هش پیام ناصحیح + هش پیام قبلی متفاوت است. + شناسه پیام ناصحیح + رسیدها فعال شوند؟ + شما + پیام‌ها و پرونده‌ها + تماس‌ها + حالت ناشناس + عبارت عبور پایگاه داده + صدور پایگاه داده + نمایه گپ حذف شود؟ + نام خود را وارد کنید: + ایجاد + روش استفاده از مارکداون + می‌توانید از مارکداون برای آرایش پیام‌ها استفاده کنید: + تماس بی‌پاسخ + تماس پذیرفته + نامتمرکز + نمایه خود را ایجاد کنید + SimpleX چگونه کار می‌کند + مخزن GitHub ما.]]> + استفاده از گپ + بهترین گزینه برای باتری. شما اعلان‌ها را فقط وقتی دریافت می‌کنید که برنامه در حال اجراست (بدون سرویس پس‌زمینه).]]> + تماس‌ها روی صفحه قفل: + پذیرفتن + سرور واسط از نشانی IP شما محافظت می‌کند، اما سرور می‌تواند مدت تماس را مشاهده کند. + گشودن + رمزگذاری سرتاسر شده + قطع تماس + ویدئو خاموش + ویدئو روشن + صدا خاموش + بلندگو خاموش + بلندگو روشن + در حال اتصال تماس + فعال کردن قفل + کد عبور تعیین شد! + کد عبور تغییر کرد! + تغییر حالت قفل + خودتخریبی + فعال کردن کد عبور خودتخریبی + تغییر حالت خودتخریبی + کد عبور خودتخریبی فعال شد! + کد عبور خودتخریبی + نام نمایشی جدید: + اگر کد عبور خودتخریبی خود را زمان باز کردن برنامه وارد کنید: + تمام اطلاعات برنامه حذف می‌شود. + این تنظیمات برای نمایه فعلی شما هستند + ارسال رسید برای %d مخاطب فعال است + غیرفعال برای همه + فعال برای همه گروه‌ها + غیرفعال کردن (نگه‌داشتن مقدارهای جایگزین شده گروه) + توقف برنامه + تم‌ها + حذف پایگاه داده + پایگاه داده گپ وارد شد + %d پرونده با اندازه کل %s + رمزگذاری سرتاسر دو لایه را ذخیره می‌کنند.]]> + نادیده گرفتن + بلوتوث + وارد کردن پایگاه داده + اشخاص فقط از طریق لینک‌هایی که به اشتراک می‌گذارید می‌توانند به شما متصل شوند. + تماس از پیش پایان یافته! + هش پیام ناصحیح + پذیرفتن خودکار تصاویر + ارسال پیش‌نمایش‌های لینک + سیستم + کد عبور برنامه + خاموش + ارسال رسید برای %d گروه فعال است + ارسال رسید برای %d گروه غیرفعال است + حمایت از SIMPLEX CHAT + پروکسی SOCKS + استفاده از کامپیوتر + آرشیو پایگاه داده جدید + آرشیو پایگاه داده قدیمی + خطا در شروع گپ + ایمن در برابر اسپم و سو استفاده + چگونه کار می‌کند + تماس تصویری + گوشی + بلندگو + هدفون‌ها + نسل بعدی پیام‌رسانی خصوصی + پایان یافت + خطا در باز کردن مرورگر + جهت صدور عبارت عبور تعیین کنید + محافظت از صفحه برنامه + شروع مجدد + ویژگی‌های آزمایشی + باز کردن پوشه پایگاه داده + گپ متوقف شده است + خطا در حذف پایگاه داده گپ + خطا در وارد کردن پایگاه داده گپ + شما گپ خود را کنترل می‌کنید! + نمایه، مخاطبان و پیام‌های تحویل داده شده شما روی دستگاهتان ذخیره می‌شوند. + نمایه فقط با مخاطبانتان به اشتراک گذاشته می‌شود. + نام نمایشی نمی‌تواند شامل نویسه‌های فاصله باشد. + ایجاد نمایه + نام نامعتبر! + برجسته + در انتظار تایید… + پاسخ دریافت شد… + تایید دریافت شد… + در حال اتصال… + در انتظار پاسخ… + تعیین عبارت عبور پایگاه داده + استفاده از عبارت عبور تصادفی + تماس صوتی + سرورهای ICE شما + تماس در جریان است + شناسه پیام ناصحیح + حریم خصوصی و امنیت + کد عبور جدید + تصدیق لغو شد + نمایش آخرین پیام‌ها + فعال کردن خودتخریبی + کد عبور برنامه با کد عبور خودتخریبی جایگزین می‌شود. + اگر کد عبور خودتخریبی خود را زمان باز کردن برنامه وارد کنید، تمام اطلاعات برنامه به صورت غیر قابل بازگشت حذف خواهد شد! + کد عبور خودتخریبی تغییر کرد! + رسیدها برای گروه‌ها فعال شوند؟ + فعال برای همه + آن‌ها در تنظیمات مخاطب و گروه قابل جایگزینی هستند. + گروه‌های کوچک (حداکثر ۲۰) + تنظیمات + اولین بن‌سازه بدون هیچ شناسه کاربری - با طرح‌ریزی خصوصی + اعلان‌های خصوصی + عبارت عبور تصادفی در تنظیمات به صورت متن آشکار ذخیره می‌شود. +\nمی‌توانید بعدا آن را تغییر دهید. + تماس تصویری رسیده + تماس صوتی رسیده + تماس تصویری رمزگذاری سرتاسر شده + تماس صوتی (رمزگذاری سرتاسر نشده) + بدون رمزگذاری سرتاسر + مخاطب رمزگذاری سرتاسر دارد + این اتفاق وقتی می‌افتد که شما یا اتصالتان از پشتیبان پایگاه داده قدیمی استفاده کرده باشید. + مذاکره مجدد رمزگذاری ناموفق بود. + رمزگذاری پرونده‌های محلی + پشتیبان‌گیری اطلاعات برنامه + قفل بعد از + تایید کد عبور + کد عبور تغییر نکرد! + ایجاد نمایه + مورب + ما هیچکدام از مخاطبان و پیام‌های(وقتی تحویل داده شدند) شما را روی سرورها ذخیره نمی‌کنیم. + رنگی + محرمانه + در حال تماس… + تماس ناموفق + تماس در جریان است + تماس پایان یافت %1$s + خطا در تماس + در حال راه‌اندازی… + متصل + در حال برقراری تماس… + تماس تصویری (رمزگذاری سرتاسر نشده) + رد کردن + تعیین کد عبور + رسیدها برای گروه‌ها غیرفعال شوند؟ + تغییر کد عبور خودتخریبی + رسیدهای تحویل ارسال شوند به + ابزارهای توسعه‌دهنده + برنامه + دستگاه + آزمایشی + اتصال شبکه + آیکون برنامه + پایگاه داده گپ + بن‌سازه پیام‌رسانی و کاربردی که از حریم خصوصی و امنیت شما محافظت می‌کند. + گزینه خوب برای باتری. سرویس پس‌زمینه هر ۱۰ دقیقه پیام‌ها را بررسی می‌کند. ممکن است تماس‌ها یا پیام‌های ضروری را از دست دهید.]]> + پیام‌ها از قلم افتادند + مرورگر وب پیش‌فرض برای تماس‌ها لازم است. لطفا مرورگر پیش‌فرض را در سیستم تنظیم کنید، و اطلاعات بیشتر را با توسعه‌دهندگان به اشتراک بگذارید. + سرور واسط فقط در زمان نیاز مورد استفاده قرار می‌گیرد. طرف دیگری قادر به مشاهده نشانی IP شما خواهد بود. + نمایش + غیرفعال + گشودن SimpleX Chat برای پذیرفتن تماس + تماس‌ها از صفحه قفل را از طریق تنظیمات فعال کنید. + مخاطب رمزگذاری سرتاسر ندارد + سرورهای WebRTC ICE + شناسه پیام بعدی نادرست است (کمتر یا برابر است با قبلی). +\nبروز این اتفاق می‌تواند به دلیل وجود اشکال نرم‌افزاری یا مورد حمله قرار گرفتن اتصال باشد. + لطفا آن را به توسعه‌دهندگان گزارش دهید. + پیام همسان + گپ متوقف شود؟ + پایگاه داده با استفاده از یک عبارت عبور تصادفی رمزگذاری شده، لطفا پیش از صدور آن را تغییر دهید. + خطا در متوقف کردن گپ + خطا در صدور پایگاه داده گپ + پایگاه داده گپ وارد شود؟ + وارد کردن + به منظور استفاده از پایگاه داده گپ وارد شده، برنامه را شروع مجدد کنید. + چند خطای غیر مهلک هنگام وارد کردن رخ داد - برای اطلاعات بیشتر می‌توانید کنسول گپ را ببینید. + پایگاه داده گپ حذف شد + به منظور ایجاد نمایه گپ جدید، برنامه را شروع مجدد کنید. + پرونده‌ها و رسانه + حذف پرونده‌ها برای تمام نمایه‌های گپ + پرونده‌ها و رسانه حذف شوند؟ + هیچ پرونده دریافتی یا ارسالی وجود ندارد + هرگز + %s ثانیه + اجرای اتصال خصوصی + جابه‌جایی از دستگاهی دیگر + یک نمایه گپ خالی با نام فراهم شده ایجاد می‌شود، و برنامه به طور معمول باز می‌شود. + اعطای اجازه‌ها در تنظیمات + این مجوز را در تنظیمات اندروید پیدا و به صورت دستی آن را اعطا کنید. + باز کردن تنظیمات + پروتکل و کد متن‌باز - هر کسی می‌تواند سرورها را راه‌اندازی کند. + رسیدها غیرفعال شوند؟ + فعال کردن (نگه‌داشتن مقدارهای جایگزین شده) + اعطای اجازه‌ها + میکروفون + دوربین + دوربین و میکروفون + اعطای اجازه‌ها برای برقراری تماس‌ها + حریم خصوصی باز تعریف شده + برای حفاظت از حریم خصوصی، به جای شناسه‌های کاربری مورد استفاده در بن‌سازه‌های دیگر، SimpleX شناسه‌هایی برای صفوف پیام دارد، جدا برای هر کدام از مخاطبان شما. + از باتری بیشتر استفاده می‌کند! سرویس پس‌زمینه همیشه در حال اجراست - اعلان‌ها به محض موجود شدن، نمایش داده می‌شوند.]]> + وقتی می‌تواند اتفاق بیفتد که: +\n۱. پیام‌ها در کلاینت فرستنده بعد از ۲ روز یا روی سرور بعد از ۳۰ روز منقضی شده باشند. +\n۲. رمزگشایی پیام ناموفق بود، چون شما یا مخاطبتان از پشتیبان پایگاه داده قدیمی استفاده استفاده کردید. +\n۳. اتصال مورد حمله قرار گرفته باشد. + تصاویر نمایه + به منظور صدور، ورود و حذف پایگاه داده گپ، گپ را متوقف کنید. هنگامی که گپ متوقف شده است، شما قادر به دریافت و ارسال پیام نخواهید بود. + پایگاه داده گپ فعلی شما حذف و توسط پایگاه داده وارد شده جایگزین خواهد شد. +\nاین عمل قابل برگشت نیست - نمایه، مخاطبان، پیام‌ها و پرونده‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت. + پایگاه داده گپ شما + این عمل قابل برگشت نیست - نمایه، مخاطبان، پیام‌ها و پرونده‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت. + ارسال رسید برای %d مخاطب غیرفعال است + این عمل قابل برگشت نیست - تمام پرونده‌ها و رسانه دریافتی حذف خواهند شد. عکس‌های با کیفیت پایین باقی خواهند ماند. + شما باید از تازه‌ترین نسخه پایگاه داده گپ خود روی فقط یک دستگاه استفاده کنید، در غیر این صورت ممکن است از بعضی از مخاطب‌ها ‌دیگر پیامی دریافت نکنید. + پیام‌ها + این عمل قابل برگشت نیست - پیام‌های ارسالی و دریافتی قدیمی‌تر از زمان انتخابی حذف خواهند شد. این کار ممکن است چندین دقیقه زمان ببرد. + خطا در تغییر تنظیمات + ذخیره عبارت عبور در تنظیمات + حذف پیام‌ها + ذخیره عبارت عبور در مخزن کلید + این تنظیمات بر پیام‌های موجود در نمایه گپ فعلی شما اعمال می‌شود + حذف خودکار پیام فعال شود؟ + برگرداندن + ارتقا و گشودن گپ + دعوت به گروه %1$s + به گروه می‌پیوندید؟ + ترک + دریافت پیام‌ها از این گروه برای شما متوقف خواهد شد. تاریخچه گپ حفظ خواهد شد. + دعوت اعضا + %d رویداد گروه + %s، %s و %d عضو دیگر متصل شدند + %s و %s + %s، %s و %d عضو + و %d رویداد دیگر + گشودن + کد امنیتی تغییر پیدا کرد + وضعیت ناشناخته + سازنده + عبارت عبور جدید… + پایگاه داده رمزگذاری و عبارت عبور در تنظیمات ذخیره خواهد شد. + عبارت عبور رمزگذاری پایگاه داده به‌روز و در تنظیمات ذخیره خواهد شد. + امکان دسترسی مخزن کلید برای ذخیره کلمه عبور پایگاه داده وجود ندارد + خطای پایگاه داده ناشناخته: %s + تایید جابه‌جایی نامعتبر + شما به این گروه پیوستید + شما ترک کردید + نمایه گروه به‌روز شد + عضو + مدیر + صاحب + حذف شد + نقش عضو جدید + عبارت عبور از تنظیمات پاک شود؟ + لطفا توجه داشته باشید: اگر عبارت عبور را از دست بدهید، قادر نخواهید بود آن را بازیابی کنید یا تغییر دهید.]]> + عبارت عبور پایگاه داده اشتباه + پرونده: %s + عبارت عبور پایگاه داده برای گشودن گپ الزامی است. + خطای ناشناخته + گشودن گپ + تلاش برای تغییر عبارت عبور پایگاه داده کامل نشد. + ارتقای پایگاه داده + تنزل پایگاه داده + تایید ارتقای پایگاه داده + گروه را ترک می‌کنید؟ + امکان دعوت مخاطبان وجود ندارد! + در حال استفاده از نمایه ناشناس برای این گروه هستید - برای جلوگیری از اشتراک‌گذاری نمایه اصلی شما، دعوت مخاطبان مجاز نیست + مخاطب حذف شد + متصل شد + ترک کرد + %1$s حذف شد + شما حذف شدید + توافق رمزگذاری + ناظر + عضو پیشین %1$s + بسط دادن انتخاب نقش + عبارت عبور رمزگذاری پایگاه داده به‌روز خواهد شد. + مستقیما متصل شد + %s متصل شد + پیوستن به گروه + به‌روزرسانی + تایید عبارت عبور جدید… + پایگاه داده رمزگذاری شود؟ + نسخه پایگاه داده ناسازگار + نمایه گروه به‌روز شد + شما نشانی را برای %s تغییر دادید + شما نشانی را تغییر دادید + مذاکره مجدد رمزگذاری مجاز است + گروه حذف شد + در حال اتصال (دعوت معرفی) + رمزگذاری سرتاسر استاندارد + در حال اتصال (پذیرفته شد) + در حال اتصال (اعلام شد) + متصل شد + کامل + در حال اتصال + ناشناخته + دعوت به گروه + %d مخاطب انتخاب شد + امکان دعوت مخاطب وجود ندارد! + پیوستن + گروه غیرفعال + دعوت‌نامه گروه دیگر اعتبار ندارد، توسط فرستنده پاک شد. + گروه پیدا نشد! + شما به گروه دعوت شیده‌اید + شما دعوت گروه را رد کردید + از %1$s دعوت شده + نقش %s به %s تغییر کرد + گروه حذف شد + مذاکره مجدد رمزگذاری الزامی است + رمزگذاری برای %s بی‌عیب است + مذاکره مجدد رمزگذاری برای %s مجاز است + رمزگذاری سرتاسر مقاوم در برابر کوانتوم + مخاطبی برای افزودن وجود ندارد + رد شدن از دعوت اعضا + انتخاب مخاطبان + پایگاه داده رمزگذاری شده! + عبارت عبور فعلی… + عضو %1$s به %2$s تغییر کرد + رمزگذاری + به‌روزرسانی عبارت عبور پایگاه داده + تعیین عبارت عبور + از مخزن کلید اندروید برای ذخیره امن عبارت عبور استفاده می‌شود - به سرویس اعلان اجازه عمل می‌دهد. + باید هر بار که برنامه شروع می‌شود عبارت عبور را وارد کنید - در دستگاه ذخیره نمی‌شود. + پایگاه داده رمزگذاری شده + مخاطب بررسی شد + پاک کردن + نمایش کنسول در پنجره جدید + می‌توانید گپ را از طریق تنظیمات برنامه / پایگاه داده یا با شروع مجدد برنامه شروع کنید. + گپ شروع شود؟ + هشدار: ممکن است بعضی از اطلاعات را از دست بدهید! + گپ متوقف شده است + خطا در رمزگذاری پایگاه داده + عبارت عبور از مخزن کلید پاک شود؟ + اعلان‌ها فقط تا زمان توقف برنامه تحویل داده خواهند شد! + پاک کردن + تعیین عبارت عبور پایگاه داده + لطفا عبارت عبور فعلی درست را وارد کنید. + پایگاه داده گپ شما رمزگذاری نشده است - برای محافظت از آن عبارت عبور تعیین کنید. + عبارت عبور به صورت متن آشکار در تنظیمات ذخیره شده است. + بعد از تغییر عبارت عبور یا شروع مجدد برنامه، عبارت عبور به صورت متن آشکار در تنظیمات ذخیره خواهد شد. + عبارت عبور پایگاه داده تغییر داده شود؟ + پایگاه داده رمزگذاری خواهد شد. + لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر نخواهید بود به گپ دسترسی پیدا کنید. + خطا در پایگاه داده + خطا در Keychain + عبارت عبور پایگاه داده با آنچه در مخزن کلید ذخیره شده متفاوت است. + خطا: %s + عبارت عبور اشتباه! + ورود عبارت عبور… + ذخیره عبارت عبور و گشودن گپ + برگرداندن پشتیبان پایگاه داده + پشتیبان پایگاه داده برگردانده شود؟ + لطفا بعد از برگرداندن پشتیبان پایگاه داده، کلمه عبور قبلی را وارد کنید. این عمل قابل برگشت نیست. + خطا در برگرداندن پایگاه داده + نسخه پایگاه داده از برنامه جدیدتر است، اما بدون جابه‌جایی تنزلی برای: %s + جابه‌جایی متفاوت در برنامه/پایگاه داده: %s / %s + جابه‌جایی‌ها: %s + شما به گروه دعوت شده‌اید. برای متصل شدن به اعضای گروه، به گروه بپیوندید. + پیوستن به صورت ناشناس + این گروه دیگر وجود ندارد. + شما دعوت گروه ارسال کردید + برای پیوستن لمس کنید + برای پیوستن به صورت ناشناس لمس کنید + %s مسدود شد + مسدود سازی %s لغو شد + نقش شما به %s تغییر کرد + شما نقش خود را به %s تغییر دادید + شما مسدود سازی %s را لغو کردید + %s و %s متصل شدند + %s، %s و %s متصل شدند + نشانی برای شما تغییر داده شد + در حال تغییر نشانی… + در حال تغییر نشانی برای %s… + در حال تغییر نشانی… + رمزگذاری بی‌عیب است + در حال توافق رمزگذاری برای %s… + توافق رمزگذاری برای %s + نویسنده + دعوت شد + در حال اتصال (معرفی شد) + نقش آغازین + مخاطبی انتخاب نشده + در حال توافق رمزگذاری… + ترک کرد + مخاطب %1$s به %2$s تغییر کرد + نشانی مخاطب حذف شد + تعیین نشانی مخاطب جدید + پایگاه داده با استفاده از عبارت عبور تصادفی رمزگذاری شده، می‌توانید آن را تغییر دهید. + بعد از شروع مجدد برنامه یا تغییر عبارت عبور، از مخزن کلید اندروید برای ذخیره امن عبارت عبور استفاده خواهد شد - اجازه دریافت اعلان‌ها را خواهد داد. + پایگاه داده رمزگذاری و عبارت عبور در مخزن کلید ذخیره خواهد شد. + عبارت عبور رمزگذاری پایگاه داده به‌روز و در مخزن کلید ذخیره خواهد شد. + عبارت عبور درست را وارد کنید. + لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر به تغییرش نخواهید بود. + عبارت عبور در مخزن کلید پیدا نشد، لطفا به صورت دستی آن را وارد کنید. دلیل این اتفاق ممکن است برگرداندن اطلاعات برنامه با استفاده از یک ابزار پشتیبان‌گیری باشد. اگر این طور نیست، لطفا با توسعه دهندگان تماس بگیرید. + تنزل و گشودن گپ + گپ متوقف شده است. اگر از پیش از این پایگاه داده روی دستگاه دیگری استفاده می‌کردید، بهتر است قبل از شروع گپ، آن را برگردانید. + به این گروه پیوستید. در حال اتصال به عضوی از گروه که از شما دعوت کرد. + دعوت منقضی شد! + دعوت گروه منقضی شد + از طریق لینک گروهتان دعوت شد + شما نقش %s را به %s تغییر دادید + شما %s را مسدود کردید + شما %1$s را حذف کردید + عکس نمایه حذف شد + تعیین عکس نمایه جدید + نمایه به‌روز شد + مذاکره مجدد رمزگذاری برای %s الزامی است + در حال ارسال از طریق + اتصال اصلاح شود؟ + اصلاح توسط عضو گروه پشتیبانی نمی‌شود + نمایه گپ شما به اعضای گروه ارسال خواهد شد + ایجاد لینک + خطا در به‌روزرسانی لینک گروه + زمان توقف پروتکل + می‌توانید این نشانی را با مخاطبان خود به اشتراک بگذارید تا به آن‌ها اجازه دهید به %s متصل شوند. + غیرفعال + رسیدها غیرفعال هستند + برای کنسول + حذف + به‌روزرسانی تنظیمات کلاینت را دوباره به سرورها متصل خواهد کرد. + نمایه گپ حذف شود؟ + خصوصی کردن نمایه! + می‌توانید نمایه کاربر را پنهان یا بی‌صدا کنید - برای نمایش منو لمس کنید و‍ نگه دارید. + لغو پنهان‌سازی نمایه + لغو پنهان‌سازی نمایه گپ + کلمه عبور نمایه + نمایه تصادفی شما + ابتدایی + پیام ارسالی + خیر + همیشه + روشن + پیام خوشامدگویی + حذف عضو + سیستم + تم + پیام دریافتی + لینک حذف شود؟ + می‌توانید یک لینک یا کد QR به اشتراک بگذارید - هر کسی می‌تواند به گروه بپیوندد. اگر بعدا گروه را حذف کنید، اعضای گروه را از دست نخواهید داد. + خطا در ایجاد لینک گروه + خطا در ارسال دعوت + اشتراک‌گذاری نشانی + این گروه بیش از %1$d عضو دارد، رسیدهای تحویل ارسال نمی‌شوند. + نام محلی + شناسه پایگاه داده + ایجاد شد در + دریافت شد در: %s + حذف شد در: %s + توسط مدیر حذف شد در: %s + ناپدید می‌شود در: %s + عضو حذف شود؟ + حذف عضو + مسدودسازی عضو + مسدودسازی + عضو برای همه مسدود شود؟ + لغو مسدودسازی + مسدودسازی عضو برای همه لغو شود؟ + مسدود شده توسط مدیر + مسدود + تغییر + تعویض + نقش گروه تغییر داده شود؟ + اتصال مستقیم؟ + درخواست اتصال به این عضو گروه ارسال خواهد شد. + پیام خوشامدگویی ذخیره شود؟ + مذاکره مجدد رمزگذاری + ذخیره نمایه گروه + به‌روزرسانی + شما هنوز تماس‌ها و اعلان‌های نمایه‌های بی‌صدا را وقتی فعال هستند دریافت می‌کنید. + ناشناس + سیستم + وارد کردن تم + شما اجازه می‌دهید + پیش‌فرض (%s) + گروه برای شما حذف خواهد شد - این عمل قابل برگشت نیست! + ترک گروه + لینک گروه + نشانی + در حال دریافت از طریق + بی‌صدا + بازنشاندن رنگ‌ها + اصلاح + شناسه پایگاه داده: %d + رکورد به‌روز شد در: %s + بدون متن + ارسال پیام مستقیم + پنهان کردن + ارسال شد در + روشن + خطا در وارد کردن تم + مخاطب اجازه می‌دهد + شما در حال دعوت از مخاطبی که با او نمایه ناشناسی به اشتراک گذاشته‌اید به گروهی هستید که در آن از نمایه اصلی خود استفاده می‌کنید + حذف گروه + افزودن پیام خوشامدگویی + وضعیت شبکه + نمایه گروه روی دستگاه‌های اعضا ذخیره می‌شود، نه روی سرورها. + اتصال‌های نمایه و سرور + (فعلی) + عضو + عضو از گروه حذف خواهد شد - این عمل قابل برگشت نیست! + مسدود برای همه + اصلاح توسط مخاطب پشتیبانی نمی‌شود + ثانیه + تمام گپ‌ها و پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! + فقط اطلاعات نمایه محلی + دعوت اعضا + گروه حذف شود؟ + گروه برای تمام اعضا حذف خواهد شد - این عمل قابل برگشت نیست! + ایجاد لینک گروه + خطا در حذف لینک گروه + دریافت شد در + حذف شد در + توسط مدیر حذف شد در + ایجاد شد در: %s + عضو مسدود شود؟ + ویرایش نمایه گروه + حذف لینک + خطا در ایجاد مخاطب عضو + %s در %s + %s (فعلی) + نقش + ذخیره و به‌روزرسانی نمایه گروه + وقتی نمایه ناشناسی را با کسی به اشتراک می‌گذارید، این نمایه برای گروه‌هایی که شما را به آن‌ها دعوت می‌کند استفاده خواهد شد. + نام گروه را وارد کنید: + ایجاد گروه + خطا در ذخیره نمایه گروه + بازنشاندن به پیش‌فرض‌ها + صدور تم + خطا در حذف عضو + خطا در تغییر نقش + ثانوی + ارسال شد در: %s + %s: %s + پیام ذخیره شده + تغییر نقش + حذف نمایه + شما: %1$s + تمام اعضای گروه متصل باقی خواهند ماند. + تنها صاحبان گروه می‌توانند تنظیمات گروه را تغییر دهند. + رسیدهای ارسال + رکورد به‌روز شد در + ناپدید می‌شود در + تمام پیام‌های %s پنهان خواهند شد! + مسدودسازی عضو لغو شود؟ + لغو مسدودسازی عضو + لغو مسدودسازی برای همه + پیام‌های %s نشان داده خواهند شد! + نقش به «%s» تغییر داده خواهد شد. تمام افراد گروه مطلع خواهند شد. + نقش به «%s» تغییر داده خواهد شد. عضو یک دعوت جدید دریافت خواهد کرد. + خطا در مسدودسازی عضو برای همه + گروه + اتصال + مستقیم + غیرمستقیم (%1$s) + پیام خوشامدگویی + پیام خوشامدگویی بیش از حد طولانی است + پیش‌نمایش + پیام خوشامدگویی را وارد کنید… + پیام بیش از حد بزرگ است + سرورها + تغییر نشانی دریافتی + اصلاح اتصال + ایجاد گروه محرمانه + تماما نامتمرکز - قابل مشاهده فقط توسط اعضا. + نام کامل گروه: + زمان توقف پروتکل در کیلوبایت + دریافت همزمان + زمان توقف اتصال TCP + وقفه پینگ + شمار پینگ + فعال کردن زنده نگه‌داشتن TCP + ذخیره + تنظیمات شبکه به‌روزرسانی شود؟ + افزودن نمایه + لغو پنهان‌سازی + لغو بی‌صدا + کلمه عبور را در جستجو وارد کنید + برای فعال‌سازی نمایه لمس کنید. + دوباره نمایش داده نشود + بی‌صدا هنگام غیرفعال بودن! + حذف نمایه گپ + حالت ناشناس از حریم خصوصی شما با استفاده از یک نمایه تصادفی جدید برای هر مخاطب محافظت می‌کند. + اجازه می‌دهد اتصال‌های بی‌نام زیادی داشته باشید بدون اطلاعات مشترک بین آن‌ها در تنها یک نمایه گپ. + تاریک + SimpleX + تم تاریک + مطمئن شوید پرونده دارای ترکیب YAML صحیح است. برای داشتن یک نمونه از ساختار پرونده تم، تم را صادر کنید. + ثانوی اضافی + ابتدایی اضافی + پس‌زمینه + منوها و هشدارها + عنوان + بله + تنظیمات مخاطب + حذف برای همه + به مخاطبان خود اجازه حذف پیام‌های ارسالی به صورت غیرقابل برگشت دهید. (۲۴ ساعت) + به مخاطبان خود اجازه افزودن واکنش‌های پیام می‌دهید. + فقط مخاطب شما می‌توانید پیام‌های ناپدید شونده ارسال کنید. + فقط شما می‌توانید پیام‌ها را به صورت غیرقابل برگشت حذف کنید (مخاطبتان می‌تواند آن‌ها را برای حذف علامت‌گذاری کند). (۲۴ ساعت) + فقط مخاطبتان می‌تواند پیام‌ها را به صورت غیرقابل برگشت حذف کند (شما می‌توانید آن‌ها را برای حذف علامت‌گذاری کنید). (۲۴ ساعت) + فقط شما می‌توانید پیام‌های صوتی ارسال کنید. + هر دوی شما و مخاطبتان می‌توانید واکنش‌های پیام اضافه کنید. + فقط شما می‌توانید واکنش‌های پیام اضافه کنید. + ارسال پیام‌های ناپدید شونده را منع می‌کنید. + اجازه ارسال پیام‌های مستقیم را به اعضا می‌دهید. + اجازه ارسال پرونده‌ها و رسانه را می‌دهید. + اعضای گروه می‌توانند پیام‌های ارسالی را به صورت غیرقابل برگشت حذف کنند. (۲۴ ساعت) + حذف بعد از + تنظیمات گفت‌و‌گو + پرونده‌ها و رسانه + تماس‌های صوتی/تصویری + " +\nموجود در نسخه 5.1" + به مخاطبان خود اجازه ارسال پیام‌های صوتی می‌دهید. + فقط زمانی اجازه حذف پیام‌ها به صورت غیرقابل برگشت را می‌دهید که مخاطب شما این اجازه را به شما بدهد. (۲۴ ساعت) + پیام‌های ناپدید شونده در این گروه ممنوع هستند. + تعیین تنظیمات گروه + واکنش‌های پیام + حذف پیام به صورت غیرقابل برگشت در این گپ ممنوع است. + %d هفته + به مخاطبان خود اجازه ارسال پیام‌های ناپدید شونده دهید. + فقط وقتی پیام‌های صوتی را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. + واکنش‌های پیام در این گپ ممنوع هستند. + ارسال پیام‌های صوتی را منع می‌کنید. + اعضای گروه می‌توانند پیام‌های صوتی ارسال کنند. + پذیرفتن + هر دوی شما و مخاطبتان می‌توانید پیام‌های ناپدید شونده ارسال کنید. + فقط شما می‌توانید پیام‌های ناپدید شونده ارسال کنید. + حذف پیام به صورت غیرقابل برگشت را منع می‌کنید. + واکنش‌های پیام‌ها را منع می‌کنید. + ارسال ۱۰۰ پیام آخر به اعضای جدید. + تا ۱۰۰ پیام آخر به اعضای جدید ارسال خواهد شد. + %d ماه + %d ماه + %d دقیقه + فقط وقتی تماس‌ها را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. + منع تماس‌های صوتی/تصویری. + ارسال پرونده‌ها و رسانه را منع می‌کنید. + اجازه ارسال لینک‌های SimpleX را می‌دهید. + تاریخچه به اعضای جدید ارسال نمی‌شود. + اعضای گروه می‌توانند واکنش‌های پیام اضافه کنند. + اعضای گروه می‌توانند لینک‌های SimpleX ارسال کنند. + لینک‌های SimpleX در این گروه ممنوع هستند. + %d ثانیه + %d دقیقه + %d ماه + مدیران + صاحبان + جدید در %s + مطالعه بیشتر + هر دوی شما و مخاطبتان می‌توانید پیام‌های صوتی ارسال کنید. + فقط وقتی واکنش‌های پیام را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. + منع واکنش‌های پیام. + فقط زمانی پیام‌های ناپدید شونده را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز بداند. + ارسال پیام‌های ناپدید شونده را منع می‌کنید. + فقط مخاطبتان می‌تواند پیام‌های صوتی ارسال کند. + %d روز + مخاطبان می‌توانند پیام‌ها را برای حذف علامت بگذارند؛ شما قادر به مشاهده آن‌ها خواهید بود. + خاموش` + تنظیمات گروه + تنظیمات شما + پیام‌های ناپدید شونده + پیام‌های مستقیم + پیام‌های صوتی + لینک‌های SimpleX + تاریخچه قابل رویت + فعال + فعال برای شما + فعال برای مخاطب + خاموش + دریافتی، ممنوع + تعیین ۱ روز + منع ارسال پیام‌ها صوتی. + به مخاطبان خود اجازه تماس با شما را می‌دهید. + پیام‌های ناپدید شونده در این گپ ممنوع هستند. + هر دوی شما و مخاطبتان می‌توانید پیام‌ها را به صورتی غیرقابل برگشت حذف کنید. (۲۴ ساعت) + هر دوی شما و مخاطبتان می‌توانید تماس برقرار کنید. + فقط شما می‌توانید تماس برقرار کنید. + فقط مخاطبتان می‌تواند تماس برقرار کند. + تماس‌های صوتی/تصویری ممنوع هستند. + اجازه ارسال پیام‌های ناپدید شونده می‌دهید. + اجازه ارسال پیام‌های صوتی را می‌دهید. + اجازه واکنش‌های پیام را می‌دهید. + ارسال لینک‌های SimpleX را منع می‌کنید + عدم ارسال تاریخچه به اعضای جدید. + اعضای گروه می‌توانند پیام‌های ناپدید شونده ارسال کنند. + اعضای گروه می‌توانند پیام‌های مستقیم ارسال کنند. + پیام‌های مستقیم بین اعضا در این گروه ممنوع هستند. + حذف غیرقابل برگشت در این گروه ممنوع است. + پیام‌های صوتی در این گروه ممنوع هستند. + واکنش‌های پیام در این گروه ممنوع هستند. + اعضای گروه می‌توانند پرونده‌ها و رسانه ارسال کنند. + پرونده‌ها و رسانه در این گروه ممنوع هستند. + %d ثانیه + %d ساعت + %d ساعت + %d ساعت + %d روز + %d روز + %d هفته + %d هفته + پیشنهاد %s + پیشنهاد %s: %2s + لغو %s + تمام اعضا + فعال برای + چی جدید است + پیام‌های صوتی در این گپ ممنوع هستند. + فقط مخاطبتان می‌تواند واکنش‌های پیام اضافه کند. + اجازه حذف پیام‌های ارسالی به صورت غیرقابل برگشت را می‌دهید. (۲۴ ساعت) + ارسال پیام‌های مستقیم به اعضا را منع می‌کنید. + ظرفیت از محدودیت فراتر رفت - گیرنده پیام‌های ارسالی پیشین را دریافت نکرد. + خطای سرور مقصد: %1$s + خطا: %1$s + هم اکنون در حال پیوستن به گروه هستید! + خطای بسیار مهم + اتصال خاتمه یافت + از مسیریابی خصوصی استفاده نشود. + رسیدهای تحویل! + فعال نشود + خطا در فعال‌سازی رسیدهای تحویل! + دستگاه‌ها + اتصال کامپیوتر قطع شود؟اتصال کامپیوتر قطع شود؟ + قطع شد، به دلیل: %s + نسخه برنامه کامپیوتر %s با این برنامه سازگار نیست. + در انتظار کامپیوتر… + اتصال به کامپیوتر + کامپیوترهای متصل + اتصال کامپیوتر قطع شد + %s در وضع بدی است]]> + به خودتان متصل می‌شوید؟ + خطا در بارگیری آرشیو + لغو جابه‌جایی + هشدار: آرشیو حذف خواهد شد.]]> + هم اکنون در حال اتصال هستید! + همیشه + همیشه از مسیریابی خصوصی استفاده شود. + اعمال + کد عبور برنامه + تماس‌های صوتی و تصویری + سرورها را به وسیله اسکن کد QR اضافه کنید. + مدیران می‌توانند یک عضو را برای همه مسدود کنند. + اجازه تنزل + لطفا توجه داشته باشید: به منظور حفاظت امنیت، استفاده از پایگاه داده یکسان در دو دستگاه، رمزگشایی پیام‌های اتصال‌های شما را از کار خواهد انداخت.]]> + جابه‌جایی از دستگاهی دیگر را انتخاب و کد QR را اسکن کنید.]]> + تایید تنظیمات شبکه + تایید کنید که عبارت عبور پایگاه داده رای برای جابه‌جایی آن به خاطر دارید. + کامپیوتر متصل شد + در حال اتصال به کامپیوتر + اتصال متوقف شد + کامپیوتر غیرفعال است + اتصال متوقف شد + دستگاه‌های کامپیوتر + کامپیوتر پیدا شد + به کامپیوتر متصل شد + تنظیمات کامپیوتر متصل + اسکن کد QR از کامپیوتر + کامپیوتر دارای کد دعوت اشتباه است + کامپیوتر دارای نسخه پشتیبانی نشده است. لطفا، اطمینان حاصل کنید که از نسخه یکسان روی هر دو دستگاه استفاده می‌کنید. + از طریق لینک متصل می‌شوید؟ + رسیدهای تحویل غیرفعال هستند! + در حال بارگیری جزئیات لینک + عبارت عبور را وارد کنید + خطا در حذف پایگاه داده + کدهای امنیتی را با مخاطبان خود مقایسه کنید. + برنامه پرونده‌های جدید محلی (به جز ویدئوها) را رمزگذاری می‌کند. + رمزگذاری پرونده‌ها و رسانه ذخیره شده + به وسیله پروتکل امن مقاوم در برابر کوانتوم. + مسدودسازی اعضای گروه + خطا + %s قطع شد، به دلیل: %s]]> + کامپیوتر + رابط چینی و اسپانیایی + به وسیله نمایه گپ (پیش‌فرض) یا به وسیله اتصال (آزمایشی). + در حین اتصال به کامپیوتر، مهلت زمان اتصال تمام شد. + روز‍ + فعال کردن + سفارشی + قطع اتصال + تایید کد با کامپیوتر + خطا در بارگذاری آرشیو + در حال آرشیو پایگاه داده + ایجاد لینک آرشیو + حذف پایگاه داده از این دستگاه + اتصال به کامپیوتر در وضع بدی است + نام‌ها، آواتارها و انزوای ترابری متفاوت. + چند چیز دیگر + کامپیوتر مشغول است + پیام‌های ناپدید شونده + قطع اتصال + پیام‌های بهتر + اتصال به صورت خودکار + قابل کشف از طریق شبکه محلی + سلولی + ایجاد نمایه جدید در برنامه کامپیوتر. 💻 + نشانی کامپیوتر + الصاق نشانی کامپیوتر + یافتن از طریق شبکه محلی + تمام اطلاعات وقتی وارد می‌شوند پاک می‌شوند. + سفارشی کردن و اشتراک‌گذاری تم‌های رنگ. + تم‌های سفارشی + - اتصال به سرویس فهرست راهنما (آزمایشی)! +\n- رسیدهای تحویل (تا ۲۰ دقیقه). +\n- سریع‌تر و پایداری بیشتر. + گروه‌های بهتر + فعال کردن در گپ های مستقیم (آزمایشی)! + اتصال با کامپیوتر قطع شود؟ + به زودی! + بارگیری موفق نبود + گپ جابه‌جا شد! + آرشیو و بارگذاری + تایید بارگذاری + اتصال اینترنت خود را بررسی و دوباره امتحان کنید + برنامه کامپیوتر جدید + وصل کردن برنامه‌های موبایل و کامپیوتر! 🔗 + یافتن و پیوستن به گروه‌ها + ایجاد یک گروه با استفاده از یک نمایه تصادفی. + جابه‌جایی اطلاعات برنامه + استفاده از کامپیوتر را در برنامه موبایل باز و کد QR را اسکن کنید.]]> + در حال بارگیری آرشیو + خطا در صدور پایگاه داده گپ + خطا در ذخیره تنظیمات + تمام مخاطبان، مکالمات و پرونده‌های شما به صورت امن، رمزگذاری و به صورت بسته‌های داده به واسطه‌های XFTP تنظیم شده، بارگذاری خواهند شد. + موبایل متصل شد + مدیران می‌توانند لینک‌ها را برای پیوستن به گروه‌ها ایجاد کنند. + قطع اتصال موبایل‌ها + به موبایل متصل شد + نام این دستگاه را وارد کنید… + (جدید)]]> + پذیرفتن خودکار درخواست‌های مخاطب + نشانی کامپیوتر ناصحیح + نسخه ناسازگار + عربی، بلغاری، فنلاندی، عبری، تایلندی و اوکراینی - با سپاس از کاربران و Weblate + سرور فرستادن: %1$s +\nخطای سرور مقصد: %2$s + سرور فرستادن: %1$s +\nخطا: %2$s + هشدار تحویل پیام + مشکلات شبکه - پیام بعد از تلاش‌های زیاد برای ارسالش منقضی شد. + نشانی سرور با تنظیمات شبکه ناسازگار است. + نسخه سرور با تنظیمات شبکه ناسازگار است. + کلید اشتباه یا اتصال ناشناخته - به احتمال زیاد این اتصال حذف شده است. + لطفا آن را به توسعه‌دهندگان گزارش دهید: +\n%s + نگه‌داشتن اتصال‌های خود + هرگز + استفاده از مسیریابی خصوصی با سرورهای ناشناخته. + استفاده از مسیریابی خصوصی با سرورهای ناشناخته وقتی نشانی IP محافظت نشده است. + حالت مسیریابی پیام + بله + پیام‌ها مستقیما فرستاده نشود، حتی اگر سرور مقصد شما از مسیریابی خصوصی پشتیبانی نکند. + گزینه پس‌رفت مسیریابی پیام + نمایش وضعیت پیام + برای محافظت از نشانی IP شما، مسیریابی خصوصی از سرورهای SMP شما به منظور تحویل پیام‌ها استفاده می‌کند. + مسیریابی پیام خصوصی + مسیریابی خصوصی + تنظیمات سرور بهبودیافته + با پیام خوشامدگویی اختیاری. + مخاطبان شما می‌توانند اجازه حذف کامل پیام را بدهند. + چندین نمایه گپ + نام‌های پرونده خصوصی + پالایش گپ‌های خوانده نشده و برگزیده. + اصلاح رمزگذاری بعد از برگرداندن پشتیبان‌ها. + مدیریت گروه + رابط کاربری ژاپنی و پرتقالی + ناپدید کردن یک پیام + استفاده باتری کاهش یافته + به جای تصدیق سیستم آن را تعیین کنید. + پشتیبانی از بلوتوث و دیگر بهبودها. + - پیام‌های صوتی تا ۵ دقیقه. +\n- زمان سفارشی برای ناپدید کردن. +\n- ویرایش تاریخچه. + پیوستن سریع‌تر و پیام‌های قابل اطمینان تر. + باز فرستادن و ذخیره پیام‌ها + مربع، دایره، و هر چیزی در این بین. + در گپ‌های مستقیم فعال خواهد شد! + ارسال رسیدهای تحویل برای تمام مخاطبان فعال خواهد شد. + می‌توانید بعدا از طریق تنظیمات آن را فعال کنید + نام این دستگاه + تایید کد در موبایل + %s قطع شد]]> + این دستگاه + در انتظار متصل شدن موبایل: + %s مشغول است]]> + تصادفی + تجدید + %s نسخه پشتیبانی نشده دارد. لطفا، اطمینان حاصل کنید که از نسخه یکسان روی هر دو دستگاه استفاده می‌کنید]]> + %s قطع شد]]> + این ویژگی هنوز پشتیبانی نمی‌شود. انتشار بعدی را امتحان کنید. + %1$s هستید.]]> + گروه از قبل وجود دارد! + شما هم اکنون در حال پیوستن به گروه از طریق این لینک هستید. + در حال وارد کردن آرشیو + یا لینک آرشیو را الصاق کنید + الصاق لینک آرشیو + می‌توانید دوباره امتحان کنید. + پرونده حذف شد یا لینک نامعتبر است + جابه‌جایی دستگاه + در حال آماده‌سازی بارگذاری + نهایی‌سازی جابه‌جایی + یا لینک پرونده را به صورت امن به اشتراک بگذارید + شروع گپ + اترنت باسیم + بهبودهای بیشتر به زودی! + سازگار نیست! + تایید اتصال‌ها + %s غیرفعال است]]> + شما از پیش اتصال به وسیله این نشانی را درخواست کرده‌اید! + شروع مجدد گپ + حتی وقتی در مکالمه غیرفعال باشند. + خیر + موبایل متصلی وجود ندارد + مدیران حالا می‌توانند: +\n- پیام‌های اعضا را حذف کنند. +\n- اعضا را غیرفعال کنند ( نقش «ناظر») + لطفا تایید کنید که تنظیمات شبکه برای این دستگاه درست هستند. + محفوظ نگه داشتن پیش‌نویس پیام آخر، به همراه ضمیمه‌ها. + درخواست اتصال تکرار شود؟ + از موبایل اسکن کنید + ارسال رسیدهای تحویل برای تمام مخاطبان در تمام نمایه‌های گپ قابل مشاهده، فعال خواهد شد. + پیام‌ها مستقیما ارسال شود وقتی نشانی IP محافظت می‌شود و سرور مقصد شما از مسیریابی خصوصی پشتیبانی نمی‌کند. + پیام‌ها مستقیما ارسال شود وقتی سرور مقصد شما از مسیریابی خصوصی پشتیبانی نمی‌کند. + شکل دادن به تصاویر نمایه + با سپاس از کاربران - از طریق Weblate همکاری کنید! + با سپاس از کاربران - از طریق Weblate همکاری کنید! + این لینک یک‌بارمصرف خودتان است! + این نشانی‌ SimpleX خودتان است! + واسطه‌های ناشناخته + محافظت نشده + تایید اتصال + تایید عبارت عبور پایگاه داده + وقتی IP پنهان است + شما هم اکنون در حال اتصال از طریق لینک یک‌بارمصرف هستید! + نمایه‌های گپ پنهان + (این دستگاه v%s)]]> + متصل کردن یک موبایل + پیش‌نویس پیام + حداکثر ۴۰ ثانیه، دریافت فوری. + پیام‌های صوتی + پیام‌های ارسال شده بعد زمان تعیین شده حذف خواهند شد. + رابط فرانسوی + به وسیله یک کلمه عبور از نمایه‌های گپ خود محافظت کنید! + بهبودهای بیشتر به زودی! + با سپاس از کاربران - از طریق Weblate همکاری کنید! + رسیدهای تحویل پیام! + %s به پایان رسید]]> + لطفا آن را به توسعه‌دهندگان گزارش دهید: +\n%s +\n +\nپیشنهاد می‌شود که برنامه را شروع مجدد کنید. + رابط ایتالیایی + - تحویل پیام پایدارتر +\n- گروه‌های کمی بهتر +\n- و بیشتر! + کد عبور خودتخریبی + با سپاس از کاربران - از طریق Weblate همکاری کنید! + ویدئوها و پرونده‌ها تا ۱ گیگابایت + تایید عبارت عبور + لینک نامعتبر + هنگام برقراری تماس‌های صوتی و تصویری. + صداهای تماس + منبع پیام خصوصی باقی خواهد ماند. + رابط کاربری لیتوانی + دقیقه + اتصال شبکه پایدارتر. + مدیریت شبکه + هفته + ماه + انتخاب + دستگاه موبایل جدید + در حال متوقف کردن گپ + بدون اتصال شبکه + دیگر + تایید امنیت اتصال + در حال آماده‌سازی بارگیری + جابه‌جایی کامل شد + نباید از یک پایگاه داده روی دو دستگاه استفاده کنید.]]> + پیام خوشامدگویی گروه + - مطلع کردن اختیاری مخاطبان حذف شده. +\n- نام‌های نمایه شامل فاصله. +\n- و بیشتر! + رابط کاربری مجارستانی و ترکی + جابه‌جایی به دستگاه دیگر از طریق کد QR. + گشودن گروه + WiFi + تماس‌های تصویر در تصویر + استفاده از برنامه در حین مکالمه. + در حال جابه‌جایی + %1$s!]]> + امنیت و حریم خصوصی بهبودیافته + پنهان کردن صفحه برنامه در برنامه‌های اخیر. + پیام‌های زنده + گیرنده‌ها به‌روزرسانی‌ها را هم‌زمان با تایپ کردن شما مشاهده می‌کنند. + برای محافظت از منطقه زمانی، پرونده‌های تصویر/صدا از UTC استفاده می‌کنند. + سریع و بدون منتظر ماندن تا زمانی که فرستنده آنلاین شود. + کاهش بیشتر استفاده باتری + تعیین پیام نمایش داده شده به اعضای جدید! + رابط لهستانی + با سپاس از کاربران - از طریق Weblate همکاری کنید! + واکنش‌های پیام + بالاخره، ما آن‌ها را داریم! 🚀 + تیک دومی که ما نداشتیم! ✅ + برای پنهان کردن پیام‌های ناخواسته. + یادداشت‌های خصوصی + با پرونده‌ها و رسانه رمزگذاری شده. + تحویل پیام بهبود یافته + ساعت + می‌توانید بعدا از طریق تنظیمات حریم خصوصی و امنیت برنامه آن‌ها را فعال کنید. + گشودن پورت در فایروال + %s مفقود است]]> + برای اجازه دادن به برنامه موبایل به کامپیوتر متصل شوید، این پورت را در فایروال خود باز کنید، اگر فعال است + خطای داخلی + جابه‌جایی به اینجا + %1$s هستید.]]> + تکرار بارگیری + وارد کردن ناموفق بود + تکرار وارد کردن + نهایی‌سازی جابه‌جایی در دستگاه دیگر. + برای ادامه دادن، گپ باید متوقف شود. + تکرار بارگذاری + %s بارگذاری شد + بارگذاری ناموفق بود + در حال بارگذاری آرشیو + می‌توانید دوباره امتحان کنید. + خطا در تایید عبارت عبور: + ارزیابی امنیت + گروه‌های ناشناس + حالت ناشناس ساده‌شده + تغییر حالت ناشناس هنگام اتصال. + پیوستن به مکالمات گروه + جهت اتصال لینک را الصاق کنید + تاریخچه اخیر و روبات فهرست راهنمای بهبودیافته. + نوار جستجو لینک‌های دعوت قبول می‌کند. + با استفاده باتری کاهش یافته. + ثانیه + رمزگذاری مقاوم در برابر کوانتوم + گروه‌های امن‌تر + تنها یک دستگاه در هر زمان می‌تواند مورد استفاده قرار گیرد + درخواست پیوستن تکرار شود؟ + به گروه خود می‌پیوندید؟ + %s بارگیری شد + پرونده صادر شده وجود ندارد + جابه‌جایی به دستگاه دیگر + هشدار: شروع گپ روی چندین دستگاه پشتیبانی نمی‌شود و باعث عدم موفقیت در تحویل پیام خواهد شد + نام دستگاه با کلاینت موبایل متصل شده به اشتراک گذاشته خواهد شد. + پیدا کردن سریع‌تر گپ‌ها + لینک‌های گروه + حذف غیرقابل برگشت پیام + امنیت SimpleX Chat به وسیله Tails of Bits مورد سنجش قرار گرفت. + انزوای ترابری + کد نشست + %1$s هستید.]]> + موبایل‌های متصل + سرورهای ناشناخته! + حفاظت از نشانی IP + برنامه از شما خواهد خواست تا بارگیری‌ها از سرورهای پرونده ناشناخته را تایید کنید (به جز .onion یا وقتی پروکسی SOCKS فعال است). + پرونده‌ها + بدون تور یا VPN، نشانی IP شما برای سرورهای پرونده قابل رویت خواهد بود. + بدون تور یا VPN، نشانی IP شما برای این واسطه‌های XFTP قابل رویت خواهد بود: +\n%1$s. + هیچ + تحویل پیام بهبود یافته + رابط کاربری فارسی + با استفاده باتری کاهش یافته. + اشکال‌زدایی تحویل + اطلاعات صف پیام + تم برنامه + تایید پرونده‌ها از سرورهای ناشناخته. + اطلاعات صف سرور: %1$s +\n +\nآخرین پیام دریافتی: %2$s + نمایش فهرست گپ در پنجره جدید + حالت تاریک + سیاه + حالت رنگ + تاریک + رنگ‌های حالت تاریک + روشن + بازنشاندن رنگ + سیستم + خطا در مقداردهی اولیه WebView. سیستم خود را به نسخه جدید به روز کنید. لطفا با توسعه‌دهنگان تماس بگیرید. +\nخطا: 9%s + رنگ‌های گپ + تم گپ + تم نمایه + پس‌زمینه کاغذدیواری + ابتدایی اضافی ۲ + تنظیمات پیشرفته + پر کردن + گنجاندن + عصر به خیر! + صبح به خیر! + پاسخ دریافتی + حذف تصویر + تکرار + مقیاس + پاسخ ارسالی + ابتدایی کاغذدیواری + تعیین تم پیش‌فرض + بازنشاندن به تم برنامه + بازنشاندن به تم کاربر + تمام حالت‌های رنگ + اعمال بر + حالت روشن + مسیریابی پیام خصوصی 🚀 + ظاهر گپ‌های خود را متمایز کنید! + تم‌های جدید گپ + از نشانی IP خود در برابر واسطه‌های پیام‌رسانی انتخاب شده توسط مخاطبانتان محافظت کنید. +\nدر تنظیمات «شبکه و سرورها» فعال کنید. + دریافت امن پرونده‌ها + پرونده یافت نشد - احتمالا حذف یا لغو شده. + کلید اشتباه یا نشانی پرونده ناشناخته - به احتمال زیاد پرونده حذف شده است. + خطای پرونده + خطای پرونده موقت + وضعیت پرونده + وضعیت پیام + وضعیت پرونده: %s + وضعیت پیام: %s + خطای کپی + لطفا بررسی کنید که تلفن همراه و کامپیوتر به شبکه محلی یکسانی متصل هستند، و فایروال کامپیوتر شما اجازه اتصال را میدهد. +\nلطفا هر مشکل دیگری را با توسعه‌دهندگان به اشتراک بگذارید. + این لینک توسط موبایل دیگری استفاده شده است، لطفا لینک جدیدی در کامپیوتر بسازید. + خطای سرور پرونده:%1$s + %1$d خطای پرونده:\n%2$s + 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 1434963263..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 @@ 30 sekuntia 5 minuuttia Salli katoavat viestit vain, jos kontaktisi sallii ne. - Lisää palvelin… + Lisää palvelin Lisää tervetuloviesti Salli ääniviestien lähettäminen. Kertakäyttölinkki @@ -94,7 +94,7 @@ Puhelu on jo päättynyt! Tumma teema Yhdistetäänkö kontaktilinkin kautta\? - Yhdistetäänkö ryhmälinkin kautta\? + Liitytäänkö ryhmään? Yhdistetäänkö kutsulinkin kautta\? Yhdistä yhdistää @@ -108,7 +108,6 @@ Tietoja SimpleX:stä Hajautettu Ääni pois päältä - ARKISTO Vaihda rooli Poista kaikilta Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti. @@ -168,9 +167,6 @@ Tumma Tunnistautuminen epäonnistui Lisää esiasetettuja palvelimia - Arkisto - Poista keskusteluarkisto\? - Luotu %1$s poistettu ryhmä yhdistää yhdistäminen (hyväksytty) @@ -255,8 +251,7 @@ Poista kontakti Yllä, sitten: Keskustelujen profiili - Jokaiselle kontaktille ja ryhmän jäsenelle käytetään erillistä TCP-yhteyttä (ja SOCKS-tunnistetietoja). -\nHuomaa: jos sinulla on useita yhteyksiä, akun ja data-liikenteen määrä voi olla huomattavasti korkeampi ja jotkin yhteydet voivat epäonnistua. + Jokaiselle kontaktille ja ryhmän jäsenelle käytetään erillistä TCP-yhteyttä (ja SOCKS-tunnistetietoja). \nHuomaa: jos sinulla on useita yhteyksiä, akun ja data-liikenteen määrä voi olla huomattavasti korkeampi ja jotkin yhteydet voivat epäonnistua.]]> Äänipuhelu Paras akulle. Saat ilmoituksia vain, kun sovellus on käynnissä (EI taustapalvelua).]]> %d tuntia @@ -326,7 +321,6 @@ Tietokannan alentaminen Tietokannan päivitys Tietokanta salataan. - Poista arkisto yhdistäminen (esittelykutsu) Poistettu klo Muuta @@ -345,7 +339,7 @@ Sekä sinä että kontaktisi voivat käyttää viestireaktioita. Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit. Sekä sinä että kontaktisi voitte soittaa puheluita. - Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä. + Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä. Chat-profiilin (oletus) tai yhteyden (BETA) perusteella. peruttu %s %d päivä @@ -456,7 +450,7 @@ Virhe roolin vaihdossa %dw Ryhmän moderointi - Näyttönimi: + Profiilin nimi: Virhe käynnistettäessä keskustelua Virhe keskustelun lopettamisessa Virhe asetuksen muuttamisessa @@ -558,7 +552,7 @@ Virheellinen QR-koodi näytä QR-koodi videopuhelussa tai jaa linkki.]]> Virheellinen palvelimen osoite! - Näyttönimi + Kirjoita nimesi: Käännä kamera Se voi tapahtua, kun sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta. Väärä pääsykoodi @@ -589,7 +583,7 @@ Vanhentunut kutsu! kutsuttu epäsuora (%1$s) - Ryhmän näyttönimi: + Kirjoita ryhmän nimi: Syötä salasana hakuun Ryhmän asetukset Ryhmän jäsenet voivat lisätä viestireaktioita. @@ -623,7 +617,6 @@ Aseta kontaktin nimi… Tallenna salasana Keystoreen Vain kontaktisi voi lähettää katoavia viestejä. - Onion-isäntiä käytetään, kun niitä on saatavilla. Portti portti %d Use .onion hosts arvoon Ei, jos SOCKS-välityspalvelin ei tue niitä.]]> @@ -661,10 +654,8 @@ TCP-yhteyden aikakatkaisu PING-määrä PING-väli - Palauta Profiili- ja palvelinyhteydet Aseta ryhmän asetukset - jos SimpleX ei sisällä käyttäjätunnuksia, kuinka se voi toimittaa viestejä\?]]> PALVELIMET Tallenna ja ilmoita kontaktille Tallenna ja ilmoita kontakteille @@ -675,7 +666,6 @@ Itsetuhoutuva pääsykoodi Käynnistä sovellus uudelleen käyttääksesi tuotua keskustelutietokantaa. Vanha tietokanta-arkisto - Pysäytä keskustelu, jotta tietokantatoiminnot voidaan ottaa käyttöön. Luo uusi keskusteluprofiili käynnistämällä sovellus uudelleen. Viestit ei koskaan @@ -759,7 +749,7 @@ Lisää parannuksia on tulossa pian! Nyt järjestelmänvalvojat voivat: \n- poistaa jäsenten viestit. -\n- poista jäsenet käytöstä (\"tarkkailija\" rooli) +\n- poista jäsenet käytöstä ("tarkkailija" rooli)
Tallenna ja ilmoita ryhmän jäsenille Lopeta Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty. @@ -842,7 +832,6 @@ GitHub-arkistostamme.]]> Säännölliset 2-kerroksisella päästä päähän -salauksella.]]> - Lue lisää GitHub-tietovarastostamme. Liitä vastaanotettu linkki Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa. Avaa SimpleX Chat hyväksyäksesi puhelun @@ -873,7 +862,6 @@ Kiitos käyttäjille – osallistu Weblaten kautta! Profiilin salasana Oletusvärit - Tallenna väri Estä ääniviestien lähettäminen. Lähetetyt viestit poistetaan asetetun ajan kuluttua. Aloita uusi keskustelu @@ -917,8 +905,6 @@ Kiitos SimpleX Chatin asentamisesta! Asetukset Lisää - Onion-isäntiä ei käytetä. - Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Yksityiset ilmoitukset Kaiutin pois päältä Kaiutin päällä @@ -957,7 +943,7 @@ Järjestelmä Vain sinä voit soittaa puheluita. Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä. - Verkko &; palvelimet + Verkko & palvelimet QR-koodi Skannaa koodi alkaa… @@ -974,7 +960,6 @@ Turvallinen jono 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. Anna edellinen salasana tietokannan varmuuskopion palauttamisen jälkeen. Tätä toimintoa ei voi kumota. - Tallenna arkisto Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja. Tietue päivitetty klo Moderoitu klo @@ -1062,7 +1047,6 @@ Odottaa tiedostoa Aloita uusi keskustelu Käyttää SimpleX Chat -palvelimia. - vastaanotetaan, kontaktiesi – palvelimet, joita käytät viestien lähettämiseen.]]> Yksityisyytesi poistit %1$s kyllä @@ -1113,8 +1097,8 @@ Viestintä- ja sovellusalusta, joka suojaa yksityisyyttäsi ja tietoturvaasi. videopuhelu %1$s JÄSENET - Rooli muuttuu muotoon \"%s\". Kaikille ryhmän jäsenille ilmoitetaan asiasta. - Rooli muuttuu muotoon \"%s\". Jäsen saa uuden kutsun. + Rooli muuttuu muotoon "%s". Kaikille ryhmän jäsenille ilmoitetaan asiasta. + Rooli muuttuu muotoon "%s". Jäsen saa uuden kutsun. Ääniviestit ovat kiellettyjä tässä keskustelussa. Ääniviestit Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta. @@ -1133,7 +1117,7 @@ SMP-palvelimesi XFTP-palvelimesi Käytä SimpleX Chat palvelimia\? - TEEMAN VÄRIT + KÄYTTÖLIITTYMÄN VÄRIT Päivitä kuljetuksen eristystila\? Voit luoda sen myöhemmin Voit paljastaa piilotetun profiilisi kirjoittamalla koko salasanan Keskusteluprofiilit-sivun hakukenttään. @@ -1157,7 +1141,6 @@ \nkontakteillesi
ICE-palvelimesi Kun saatavilla - Päivitä .onion-isäntien asetus\? Käytä .onion-isäntiä Puhelusi Tätä ryhmää ei enää ole olemassa. @@ -1383,4 +1366,139 @@ 6 uutta käyttöliittymän kieltä Löydä ryhmiä ja liity niihin Luo uusi profiili työpöytäsovelluksessa. 💻 - \ No newline at end of file + Virheellinen linkki + Virheellinen QR-koodi + Ei verkkoyhteyttä + %s ladattu + Päivitä + %s ja %s + Istuntokoodi + Vahvista tietokannan tunnuslause + %1$s!]]> + Tämä on oma SimpleX-osoitteesi! + Vahvista tunnuslause + %1$s.]]> + Salli SimpleX-linkkien lähettäminen. + Lisää kontakti + Tai näytä tämä koodi + Lataus epäonnistui + Historiaa ei lähetetä uusille jäsenille. + Arkistoidaan tietokanta + Varoitus: arkisto poistetaan.]]> + Kamera ja mikrofoni + Tarkista internetyhteys ja yritä uudelleen + Yhdistä automaattisesti + Poista tietokanta tältä laitteelta + Virhe selainta avatessa + Tiedoston tila: %s + Laajenna + Hyvää huomenta! + Lisäasetukset + Aseta oletusteema + Vaalea tila + Virhe: %1$s + kaikki jäsenet + Aseta tunnuslause + Älä lähetä historiaa uusille jäsenille. + Kyllä + Tiedosto poistettiin tai linkki on virheellinen + Äänipuhelu + Lopeta puhelu + Videopuhelu + Napauta skannataksesi + Kehittäjävalinnat + Lataa + Tiedostovirhe + Näytä viestin tila + Bluetooth + Kaiutin + Kuulokkeet + Vahvista yhteydet + Sovelluksen teema + Kaikki väritilat + Ryhmäjäsenet voivat lähettää SimpleX-linkkejä. + (uusi)]]> + VIestiä ei voi lähettää + SimpleX-linkit eivät ole sallittuja + Ladataan tiedostoa + Kaikki viestit poistetaan - tätä ei voi perua! + Virheellinen nimi! + Profiilin teema + %s yhdistetty + %s, %s ja %d jäsentä + tuntematon tila + Virhe kutsua lähettäessä + Tiedoston tila + Viestin tila + Viestin tila: %s + Poistetaanko jäsen? + Luo ryhmä + Musta + Väritila + Tumma + Tumman tilan värit + Tumma tila + Hyvää iltapäivää! + Laitteet + Kirjoita tunnuslause + Virhe tietokantaa poistaessa + Viimeistele migraatio toisella laitteella. + tallennettu + Tallennettu + Toteuta + Tuonti epäonnistui + WiFi + Kiinteä ethernet + Sisäinen virhe + Vahvista verkkoasetukset + Viimeistele migraatio + Virheellinen näyttönimi! + Napauta liittääksesi linkin + Virheellinen tiedostopolku + Verkkoyhteys + Katkaise yhteys + Kamera ei saatavilla + Luodaan linkki… + Tai skannaa QR-koodi + Aina + Ei koskaan + Ei + Kamera + Avaa asetukset + Suojaa IP-osoite + TIEDOSTOT + Profiilikuvat + tuntematon + Poista jäsen + SimpleX-linkit + Kvanttiturvallinen salaus + Verkon hallinta + Kirjoita tämän laitteen nimi… + Vahvista yhteys + Työpöytälaitteet + Ei yhteensopiva! + Satunnainen + Ryhmä on jo olemassa! + Kriittinen virhe + Virhe asetuksia tallentaessa + Virhe + Tietokannan migraatio on käynnissä. +\nTämä saattaa kestää muutaman minuutin. + Yritä uudelleen + Näytä sisäiset virheet + Näytä hitaat API-kutsut + Luo profiili + Tämä on oma kertakäyttöinen linkkisi! + Virhe + Hidas funktio + Napauta yhdistääksesi + Viesti on liian pitkä + Virhe viestiä luotaessa + Mikrofoni + Vaalea + Järjestelmä + Poista kuva + Yhteensopimaton versio + Uusi mobiililaite + Tämä laite + 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 77f0abbf2d..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 @@ Notifications instantanées ! Les notifications instantanées sont désactivées ! Il peut être désactivé via les paramètres - les notifications seront toujours affichées lorsque l\'application est en cours d\'exécution.]]> - 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. + 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. Notifications périodiques Les notifications périodiques sont désactivées ! Une phrase secrète est nécessaire - autoriser SimpleX à fonctionner en arrière-plan dans la fenêtre de dialogue suivante. Sinon, les notifications seront désactivées.]]> + Autoriser le dans la boîte de dialogue suivante pour recevoir des notifications instantanément.]]> Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe L\'application récupère périodiquement les nouveaux messages - elle utilise un peu votre batterie chaque jour. L\'application n\'utilise pas les notifications push - les données de votre appareil ne sont pas envoyées aux serveurs. - SimpleX service de fond - il utilise quelques pour cent de la batterie par jour.]]> + SimpleX fonctionne en arrière-plan au lieu d\'utiliser les notifications push.]]> Cacher - Afficher l\'aperçu + Aperçu affiché Nom du contact Contact masqué : nouveau message @@ -147,8 +147,8 @@ Vous n\'avez aucune discussion Trop d’images ! Partager le fichier… - Attacher - Annuler l’aperçu d’image + Joindre + Annuler l\'aperçu de l\'image Annuler l’aperçu du fichier échec d’envoi non lu @@ -275,7 +275,7 @@ E-mail Se connecter L\'application peut recevoir des notifications uniquement lorsqu\'elle est en cours d\'exécution, aucun service d\'arrière-plan ne sera lancé. - Le service d\'arrière-plan fonctionne en permanence. Les notifications s\'affichent dès que les messages sont disponibles. + Le service est toujours en cours d’exécution en arrière-plan, les notifications s’afficheront dès que les messages seront disponibles. Afficher le contact et le message Masquer le contact et le message Connectez-vous en utilisant votre identifiant @@ -302,7 +302,7 @@ Lien invalide ! Ce lien n\'est pas un lien de connexion valide ! Demande de connexion envoyée ! - Le fichier sera reçu quand votre contact sera en ligne, merci d\'attendre ou de revenir plus tard ! + Le fichier sera reçu lorsque votre contact sera en ligne, veuillez patienter ou vérifier plus tard ! Message vocal… La taille maximale supportés des fichiers actuellement est de %1$s. Message vocal (%1$s) @@ -313,7 +313,7 @@ Scannez le code de sécurité depuis l\'application de votre contact. Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils. scanner un code QR lors d\'un appel vidéo, ou votre contact peut partager un lien d\'invitation.]]> - Ajouter un serveur… + Ajouter un serveur Markdown dans les messages Ajouter des serveurs prédéfinis Utiliser les serveurs SimpleX Chat \? @@ -321,9 +321,6 @@ Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne. Accéder aux serveurs via un proxy SOCKS sur le port %d \? Le proxy doit être démarré avant d\'activer cette option. Utiliser les hôtes .onions - Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles. - Les hôtes .onion seront nécessaires pour la connexion. - transmettre ainsi que par quel·s serveur·s vous pouvez recevoir les messages de vos contacts.]]> Vos paramètres SimpleX Lock Console du chat @@ -344,7 +341,7 @@ Vos serveurs ICE Configurer les serveurs ICE Paramètres réseau avancés - Paramètres réseau + Paramètres avancés Utiliser un proxy SOCKS \? Utiliser une connexion Internet directe \? 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. @@ -372,13 +369,12 @@ réponse reçu… confimation reçu… connexion… - Protocole et code open-source – n\'importe qui peut heberger un serveur. - 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. - Plus d\'informations sur notre GitHub. + N\'importe qui peut heberger un serveur. + Pour protéger votre vie privée, SimpleX utilise des identifiants distincts pour chacun de vos contacts. Collez le lien que vous avez reçu Utiliser le chat Notifications privées - Peut être modifié ultérieurement via les paramètres. + Comment il affecte la batterie Quand l\'application fonctionne Périodique Instantanée @@ -414,12 +410,10 @@ Comment faire Serveurs ICE (un par ligne) Erreur lors de la sauvegarde des serveurs ICE - Mettre à jour le paramètre des hôtes .onion \? Quand disponible Les hôtes .onion ne seront pas utilisés. Les hôtes .onion seront nécessaires pour la connexion. \nAttention : vous ne pourrez pas vous connecter aux serveurs sans adresse .onion. - Les hôtes .onion ne seront pas utilisés. Supprimer l\'adresse \? Tous vos contacts resteront connectés. Partager le lien @@ -444,23 +438,22 @@ en attente de confirmation… connecté terminé - La nouvelle génération de messagerie privée + L\'avenir de la messagerie La vie privée redéfinie - La 1ère plateforme sans aucun identifiant d\'utilisateur – privée par design. - Protégé du spam et des abus - On ne peut se connecter à vous qu’avec les liens que vous partagez. + Aucun identifiant d\'utilisateur. + Protégé du spam + Vous choisissez qui peut se connecter. Décentralisé Créez votre profil Établir une connexion privée Comment ça fonctionne Comment SimpleX fonctionne - si SimpleX n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?]]> - chiffrement de bout en bout à deux couches.]]> + Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages. GitHub repository.]]> - Batterie peu utilisée. Le service de fond vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]> - Batterie plus utilisée ! Le service de fond est toujours en cours d\'exécution - les notifications s\'affichent dès que les messages sont disponibles.]]> + Batterie peu utilisée. L\'app vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]> + Consomme davantage de batterie L\'app fonctionne toujours en arrière-plan - les notifications s\'affichent instantanément.]]> %1$d message(s) manqué(s) - ID de message incorrecte + ID du message incorrect PARAMÈTRES 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. @@ -478,7 +471,6 @@ 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. 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. Erreur de restauration de la base de données - Créé le %1$s appel vidéo (chiffrement de bout en bout) appel audio (sans chiffrement) appel audio (chiffrement de bout en bout) @@ -571,7 +563,7 @@ Vous avez envoyé une invitation de groupe a quitté Haut-parleur ON - Envoi d\'aperçus de liens + Aperçu des liens Erreur lors de la suppression de la base de données du chat Erreur lors de l\'arrêt du chat Erreur lors de l\'exportation de la base de données du chat @@ -583,7 +575,7 @@ %1$s veut se connecter à vous via Vos appels Se connecter via relais - Appels en écran verrouillé : + Appels sur l\'écran de verrouillage : Afficher Désactiver Vos serveurs ICE @@ -603,12 +595,12 @@ Appel manqué Appel en connexion Répondre à l\'appel - hash de message incorrect + mauvais hachage du message message dupliqué Messages manqués Vie privée et sécurité Protéger l\'écran de l\'app - Images auto-acceptées + Acceptation automatique des images Sauvegarde des données de l\'app VOUS AIDE @@ -625,7 +617,6 @@ Erreur lors de l\'importation de la base de données du chat Base de données du chat importée Supprimer le profil du chat \? - Arrêter le chat pour agir sur la base de données. Supprimer les fichiers et médias \? 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. Aucun fichier reçu ou envoyé @@ -647,7 +638,7 @@ Changer la phrase secrète de la base de données \? La base de données sera chiffrée. Erreur de la keychain - Fichier : %s + Fichier : %s La phrase secrète de la base de données est nécessaire pour ouvrir le chat. Enregistrer la phrase secrète et ouvrir le chat Ouvrir le chat @@ -656,11 +647,6 @@ Restaurer Le chat est arrêté Vous pouvez lancer le chat via les Paramètres / la Base de données de l\'app ou en la redémarrant. - Archives du chat - ARCHIVE DU CHAT - Enregistrer l\'archive - Supprimer l\'archive - Supprimer l\'archive du chat \? Invitation au groupe %1$s Rejoindre le groupe \? Vous êtes invité·e dans un groupe. Rejoignez le pour vous connecter avec ses membres. @@ -714,7 +700,7 @@ Envoyer un message dynamique Envoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez Envoyer - Le rôle sera changé pour «%s». Le membre va recevoir une nouvelle invitation. + Son rôle est désormais %s. Le membre recevra une nouvelle invitation. LIVE Inviter des membres 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. @@ -725,7 +711,7 @@ Seuls les propriétaires du groupe peuvent modifier les préférences du groupe. POUR TERMINAL Changer le rôle du groupe \? - Le rôle sera changé pour «%s». Les membres du groupe seront notifiés. + Son rôle est désormais %s. Tous les membres du groupe en seront informés. Contact vérifié⸱e Effacer %d contact·s sélectionné·e·s @@ -767,8 +753,8 @@ Système Autoriser l\'envoi de messages directs aux membres. Interdire l\'envoi de messages directs aux membres. - Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures) - La suppression irréversible de messages est interdite dans ce groupe. + Les membres peuvent supprimer de manière irréversible les messages envoyés. (24 heures) + La suppression irréversible de messages est interdite. Envoi via État du réseau Changer d\'adresse de réception @@ -783,7 +769,6 @@ Clair Sombre Thème - Enregistrer la couleur Réinitialisation des couleurs Principale Vous autorisez @@ -804,8 +789,8 @@ Seulement vous pouvez envoyer des messages éphémères. Vous seul pouvez envoyer des messages vocaux. Autoriser la suppression irréversible de messages envoyés. (24 heures) - Les messages éphémères sont interdits dans ce groupe. - Les membres du groupe peuvent envoyer des messages vocaux. + Les messages éphémères sont interdits. + Les membres peuvent envoyer des messages vocaux. Supprimer après %d sec %ds @@ -833,8 +818,7 @@ Autorise votre contact à envoyer des messages éphémères. directe Entièrement décentralisé – visible que par ses membres. - Les membres du groupes peuvent envoyer des messages éphémères. - Revenir en arrière + Les membres peuvent envoyer des messages éphémères. Interdire l’envoi de messages éphémères. Le mode incognito protège votre vie privée en utilisant un nouveau profil aléatoire pour chaque contact. La mise à jour des ces paramètres reconnectera le client à tous les serveurs. @@ -844,8 +828,8 @@ Autoriser la suppression irréversible des messages uniquement si votre contact vous l\'autorise. (24 heures) Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé). (24 heures) Seulement votre contact peut envoyer des messages éphémères. - Vous et votre contact êtes tous deux en mesure d\'envoyer des messages éphémères. - Les messages vocaux sont interdits dans ce groupe. + Vous et votre contact pouvez envoyer des messages éphémères. + Les messages vocaux sont interdits. Saisir le nom du groupe : indirecte (%1$s) Groupe @@ -858,9 +842,9 @@ Réinitialisation des valeurs par défaut Délai du protocole Intervalle de PING - Vous et votre contact êtes tous deux en mesure de supprimer de manière irréversible les messages envoyés. (24 heures) + Vous et votre contact pouvez supprimer de manière irréversible les messages envoyés. (24 heures) La suppression irréversible de message est interdite dans ce chat. - Vous et votre contact êtes tous deux en mesure d\'envoyer des messages vocaux. + Vous et votre contact pouvez envoyer des messages vocaux. Seul votre contact peut envoyer des messages vocaux. Les messages vocaux sont interdits dans ce chat. Les messages éphémères sont interdits dans cette discussion. @@ -869,8 +853,8 @@ Autorise l’envoi de messages éphémères. Interdire l’envoi de messages éphémères. Interdire la suppression irréversible des messages. - Les membres du groupe peuvent envoyer des messages directs. - Les messages directs entre membres sont interdits dans ce groupe. + Les membres peuvent envoyer des messages directs. + Les messages directs entre membres sont interdits dans ce groupe. Les destinataires voient les mises à jour au fur et à mesure que vous les tapez. Vérifier la sécurité de la connexion Comparez les codes de sécurité avec vos contacts. @@ -888,7 +872,7 @@ Les messages envoyés seront supprimés après une durée déterminée. Messages dynamiques Accepter - Demandes de contact auto-acceptées + Acceptation automatique des demandes de contact Quoi de neuf \? Les admins peuvent créer les liens qui permettent de rejoindre les groupes. Définir 1 jour @@ -921,8 +905,7 @@ Profil et connexions au serveur Transport isolé Mettre à jour le mode d\'isolement du transport \? - Une connexion TCP distincte (et identifiant SOCKS) sera utilisée pour chaque contact et membre de groupe. -\nVeuillez 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. + pour chaque contact et membre de groupe. \nVeuillez 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.]]> Profil de chat Ajouter un profil Données de profil local uniquement @@ -933,11 +916,11 @@ Vous avez déjà un profil de chat avec ce même nom affiché. Veuillez choisir un autre nom. Nom d\'affichage en double ! Interface en français - Par profil de chat (par défaut) ou par connexion (BETA). + Par profil de discussion (par défaut) ou par connexion (BETA). Interface en italien Brouillon de message - Plus d\'améliorations à venir ! - Différents profils de chat + D\'autres améliorations sont à venir ! + Profils de discussion multiples Conserver le brouillon du dernier message, avec les pièces jointes. Réduction de la consommation de batterie Noms de fichiers privés @@ -964,7 +947,7 @@ Ajouter un message d\'accueil Modération de groupe Cacher - Mute en cas d\'inactivité ! + Mise en sourdine en cas d\'inactivité ! Confirmer le mot de passe Réduction accrue de l\'utilisation de la batterie Interface en chinois et en espagnol @@ -977,7 +960,7 @@ Profils de chat cachés Désormais, les administrateurs peuvent : \n- supprimer les messages des membres. -\n- désactiver des membres (rôle \"observateur\") +\n- désactiver des membres (rôle "observateur") Enregistrer le message d\'accueil ? Choisissez un message à l\'attention des nouveaux membres ! Masquer le profil @@ -1024,7 +1007,7 @@ Trop de vidéos ! Vidéo Vidéo envoyée - La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer. + La vidéo ne sera reçue que lorsque votre contact aura fini la mettre en ligne. En attente de la vidéo En attente de la vidéo La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard ! @@ -1034,8 +1017,8 @@ Supprimer le fichier Erreur lors de la sauvegarde des serveurs XFTP Assurez-vous que les adresses des serveurs XFTP sont au bon format, séparées par des lignes et qu\'elles ne sont pas dupliquées. - Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe - Transférer le fichier + Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe + Téléverser le fichier Serveurs XFTP Vos serveurs XFTP Comparer le fichier @@ -1052,7 +1035,7 @@ SimpleX Lock n\'est pas activé ! Authentification du système Authentification - Echec de l\'authentification + Échec de l’authentification Modifier le code d\'accès Code d\'accès actuel %d minutes @@ -1072,18 +1055,17 @@ Code d\'accès défini ! Système Authentification annulée - Mauvais ID de message - Le hash du message précédent est différent. + ID du message incorrect + Le hash du message précédent est différent.\" L\'ID du message suivant est incorrect (inférieur ou égal au précédent). \nCela peut se produire en raison d\'un bug ou lorsque la connexion est compromise. Erreur de déchiffrement Cela peut se produire lorsque vous ou votre contact avez utilisé une ancienne sauvegarde de base de données. - Mauvais hash de message + Mauvais hachage du message Autoriser les appels que si votre contact les autorise. Autorise vos contacts à vous appeler. Appels audio/vidéo - " -\nDisponible dans la v5.1" + \nDisponible dans la v5.1 Interdire les appels audio/vidéo. Le fichier sera supprimé des serveurs. Révoquer @@ -1092,7 +1074,7 @@ L\'envoi du fichier sera interrompu. Veuillez le signaler aux développeurs. Soumettre - Vous et votre contact pouvez tous deux passer des appels. + Vous et votre contact pouvez passer des appels. Vous seul pouvez passer des appels. Les appels audio/vidéo sont interdits. Confirmer le code d\'accès @@ -1121,7 +1103,7 @@ Vous ne perdrez pas vos contacts si vous supprimez votre adresse ultérieurement. Adresse SimpleX Vous pouvez accepter ou refuser les demandes de contacts. - COULEURS DU THÈME + COULEURS DE L\'INTERFACE Vos contacts resteront connectés. Partager l\'adresse avec vos contacts \? Partager avec vos contacts @@ -1136,7 +1118,7 @@ Partager l\'adresse Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec %s. Aperçu - Fond d\'écran + Arrière-plan Thème sombre Exporter le thème Importer un thème @@ -1150,7 +1132,7 @@ 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. Secondaire supplémentaire Tous vos contacts resteront connectés. La mise à jour du profil sera envoyée à vos contacts. - Auto-accepter + Acceptation automatique Créer une adresse SimpleX Personnaliser le thème Continuer @@ -1161,14 +1143,14 @@ Bonjour ! \nContactez-moi via SimpleX Chat : %s Si vous ne pouvez pas vous rencontrer en personne, montrez le code QR lors d\'un appel vidéo ou partagez le lien. - Ouvrir les profils de chat + Changer de profil de discussion Menus et alertes Message reçu Assurez-vous que le fichier a une syntaxe YAML correcte. Exporter le thème pour avoir un exemple de la structure du fichier du thème. La mise à jour du profil sera envoyée à vos contacts. Guide de l\'utilisateur.]]> Enregistrer les paramètres de validation automatique - Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l\'application. + Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l\'app. Le code d\'accès de l\'application est remplacé par un code d\'autodestruction. Activer l\'autodestruction Un profil de chat vierge portant le nom fourni est créé et l\'application s\'ouvre normalement. @@ -1198,8 +1180,8 @@ Vous seul pouvez ajouter des réactions aux messages. Autoriser les réactions aux messages. Interdire les réactions aux messages. - Les membres du groupe peuvent ajouter des réactions aux messages. - Les réactions aux messages sont interdites dans ce groupe. + Les membres peuvent ajouter des réactions aux messages. + Les réactions aux messages sont interdites. heures minutes secondes @@ -1246,13 +1228,13 @@ Envoyé le : %s Message envoyé aucun texte - Des erreurs non fatales se sont produites lors de l\'importation - vous pouvez consulter la console de chat pour plus de détails. + L\'importation a entraîné des erreurs non fatales : Les notifications ne fonctionnent pas tant que vous ne relancez pas l\'application Arrêt \? Mise à l\'arrêt Redémarrer APP - Annuler + Abandonner Erreur lors de l\'annulation du changement d\'adresse Abandonner le changement d\'adresse \? Annuler le changement d\'adresse @@ -1267,8 +1249,8 @@ Délai d\'attente du protocole par KB Fichiers et médias interdits ! Permet l\'envoi de fichiers et de médias. - Les membres du groupe peuvent envoyer des fichiers et des médias. - Les fichiers et les médias sont interdits dans ce groupe. + Les membres peuvent envoyer des fichiers et des médias. + Les fichiers et les médias sont interdits. Correction non prise en charge par un membre du groupe ENVOYER DES ACCUSÉS DE RÉCEPTION AUX Le chiffrement fonctionne et le nouvel accord de chiffrement n\'est pas nécessaire. Cela peut provoquer des erreurs de connexion ! @@ -1290,7 +1272,7 @@ Activer pour tous Activer (conserver les remplacements) chiffrement accepté pour %s - Filtrer les messages non lus et favoris. + Filtrer les favoris et les messages non lus. Recherche de message plus rapide En réponse à - une diffusion plus stable des messages. @@ -1363,7 +1345,7 @@ Utiliser le profil actuel Utiliser un nouveau profil incognito Brouillon de message - Voir les derniers messages + Aperçu des derniers messages %s et %s sont connecté.es %s, %s et %d autres membres sont connectés %s, %s et %s sont connecté.es @@ -1441,7 +1423,7 @@ Les messages de %s seront affichés ! Erreur lors de l\'envoi de l\'invitation Vous avez partagé un chemin de fichier non valide. Signalez le problème aux développeurs de l\'application. - Bloquer ce membre ? + Bloquer ce membre ? %d événements de groupe Nom invalide ! %1$s !]]> @@ -1497,7 +1479,7 @@ Se déconnecter auteur Connecté au portable - Mauvaise adresse de bureau + Adresse de bureau incorrecte Coller l\'adresse du bureau Vérifier le code avec le bureau Scannez le code QR du bureau @@ -1533,14 +1515,14 @@ Envoi des 100 derniers messages aux nouveaux membres. Ajouter un contact : pour créer un nouveau lien d\'invitation ou se connecter via un lien que vous avez reçu.]]> Ne pas envoyer d\'historique aux nouveaux membres. - Ou présenter ce code + Ou montrez ce code Les 100 derniers messages sont envoyés aux nouveaux membres. Le code scanné n\'est pas un code QR de lien SimpleX. Le texte collé n\'est pas un lien SimpleX. Autoriser l\'accès à la caméra Vous pouvez à nouveau consulter le lien d\'invitation dans les détails de la connexion. Conserver l\'invitation inutilisée ? - Partager ce lien d\'invitation unique + Partagez ce lien d\'invitation unique Créer un groupe : pour créer un nouveau groupe.]]> Historique visible Code d\'accès à l\'app @@ -1549,7 +1531,7 @@ Création d\'un lien… Ou scanner le code QR Code QR invalide - Ajouter le contact + Ajouter un contact Appuyez pour scanner Conserver Appuyez pour coller le lien @@ -1646,7 +1628,7 @@ Erreur lors de l\'envoi de l\'archive 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. Appliquer - Archiver et transférer + Archiver et téléverser Archivage de la base de données Avertissement : l\'archive sera supprimée.]]> Remarque : 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é.]]> @@ -1697,7 +1679,7 @@ %s envoyé Envoi de l\'archive Finaliser le transfert - Transfert terminé + Migration terminée Démarrer le chat ne devez pas utiliser la même base de données sur deux appareils.]]> Vérifier la phrase secrète de la base de données @@ -1708,7 +1690,7 @@ Échec de l\'importation Lien invalide Transférer ici - Transfert + Migration Ou coller le lien de l\'archive Ou partagez en toute sécurité le lien de ce fichier Coller le lien de l\'archive @@ -1734,10 +1716,10 @@ tous les membres Autorise l\'envoi de liens SimpleX. Activé pour - Les membres du groupe peuvent envoyer des liens SimpleX. + Les membres peuvent envoyer des liens SimpleX. propriétaires Interdire l\'envoi de liens SimpleX - Les liens SimpleX sont interdits dans ce groupe. + Les liens SimpleX sont interdits. Pas de connexion au réseau WiFi Ethernet câblé @@ -1769,4 +1751,597 @@ Carré, circulaire, ou toute autre forme intermédiaire. Lors des appels audio et vidéo. Interface utilisateur en lituanien - \ No newline at end of file + Avertissement sur la distribution des messages + L\'adresse du serveur est incompatible avec les paramètres du réseau. + La version du serveur est incompatible avec les paramètres du réseau. + Toujours utiliser le routage privé. + Ne pas utiliser de routage privé. + Utiliser le routage privé avec des serveurs inconnus lorsque l\'adresse IP n\'est pas protégée. + Envoyez les messages de manière directe lorsque votre serveur ou le serveur de destination ne prend pas en charge le routage privé. + 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é. + Problèmes de réseau - le message a expiré après plusieurs tentatives d\'envoi. + Clé erronée ou connexion non identifiée - il est très probable que cette connexion soit supprimée. + Serveur de transfert : %1$s +\nErreur au niveau du serveur de destination : %2$s + Serveur de transfert : %1$s +\nErreur : %2$s + Toujours + Routage privé + Autoriser la rétrogradation + Mode de routage des messages + Jamais + Serveurs inconnus + Non + Lorsque l\'IP est masquée + Oui + Rabattement du routage des messages + Afficher le statut du message + Protection de l\'adresse IP + FICHIERS + ROUTAGE PRIVÉ DES MESSAGES + Erreur au niveau du serveur de destination : %1$s + Erreur : %1$s + Capacité dépassée - le destinataire n\'a pas pu recevoir les messages envoyés précédemment. + 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é). + Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé. + Pour protéger votre adresse IP, le routage privé utilise vos serveurs SMP pour délivrer les messages. + Non protégé + Serveurs inconnus ! + Sans Tor ou un VPN, votre adresse IP sera visible par les serveurs de fichiers. + Utiliser le routage privé avec des serveurs inconnus. + Sans Tor ou un VPN, votre adresse IP sera visible par ces relais XFTP : +\n%1$s. + Accentuation supplémentaire 2 + Paramètres avancés + Noir + Appliquer à + Debug de la distribution + Remplir + Bonjour Alice ! + Amélioration de la transmission des messages + Donnez à vos discussions un style différent ! + Info sur la file des messages + Nouveaux thèmes de discussion + aucun + Routage privé des messages 🚀 + Protégez votre adresse IP des relais de messagerie choisis par vos contacts. +\nActivez-le dans les paramètres *Réseau et serveurs*. + Réinitialiser au thème de l\'utilisateur + Afficher la liste des chats dans une nouvelle fenêtre + Teinte du fond d\'écran + Fond d\'écran + info sur la file du serveur : %1$s +\n +\ndernier message reçu : %2$s + Thème de l\'app + Mode de couleur + Sombre + Couleurs du mode sombre + Salut Bob ! + Réponse reçue + Retirer l\'image + Réinitialiser la couleur + Réponse envoyée + Répéter + Dimension + Tous les modes de couleur + Mode sombre + Adapter + Mode clair + Réinitialiser au thème de l\'app + Définir le thème par défaut + Confirmer les fichiers provenant de serveurs inconnus. + UI en persan + Réception de fichiers en toute sécurité + Consommation réduite de la batterie. + Couleurs de la discussion + Thème de la discussion + Thème de profil + Clair + Système + Erreur d\'initialisation de WebView. Mettez votre système à jour avec la nouvelle version. Veuillez contacter les développeurs. +\nErreur : %s + Fichier introuvable - le fichier a probablement été supprimé ou annulé. + Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé. + Erreur du serveur de fichiers : %1$s + Erreur de fichier + Erreur de fichier temporaire + Statut du fichier + Statut du fichier : %s + Statut du message + Statut du message: %s + Erreur de copie + 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. + Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le desktop. + Impossible d\'envoyer le message + Les paramètres de chat sélectionnés ne permettent pas l\'envoi de ce message. + Connections actives + Tous les profiles + Reçu avec accusé de réception + La mise à jour de l\'app est téléchargée + Complétées + Profil actuel + Erreurs de déchiffrement + désactivé + Supprimées + Désactiver + Téléchargement de la mise à jour de l\'appli, ne pas fermer l\'appli + Téléchargement %s (%s) + Erreur de reconnexion au serveur + inactif + Scanner / Coller un lien + Le message peut être transmis plus tard si le membre devient actif. + Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire. + Sessions de transport + L\'adresse du serveur est incompatible avec les paramètres réseau : %1$s. + La version du serveur est incompatible avec votre application : %1$s. + Erreur de routage privé + Veuillez essayer plus tard. + Membre inactif + Message transféré + Pas de connexion directe pour l\'instant, le message est transmis par l\'administrateur. + Serveurs connectés + Serveurs précédemment connectés + Serveurs routés via des proxy + Vous n\'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages. + Reconnecter le serveur ? + Reconnecter les serveurs ? + Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire. + Erreur de réinitialisation des statistiques + Réinitialiser + Les statistiques des serveurs seront réinitialisées - il n\'est pas possible de revenir en arrière ! + Téléversé + Statistiques détaillées + Messages envoyés + Serveur SMP + Chunks supprimés + Chunks téléchargés + Fichiers téléchargés + Erreurs de téléchargement + Adresse du serveur + Erreurs de téléversement + Bêta + Vérifier les mises à jour + Vérifier les mises à jour + Serveurs SMP configurés + Serveurs XFTP configurés + Désactivé + Installé avec succès + Installer la mise à jour + Ouvrir l\'emplacement du fichier + Autres serveurs SMP + Autres serveurs XFTP + Veuillez redémarrer l\'application. + Rappeler plus tard + Afficher le pourcentage + Sauter cette version + Stable + Pour être informé des nouvelles versions, activez la vérification périodique des versions Stable ou Bêta. + Mise à jour disponible : %s + Téléchargement de la mise à jour annulé + Taille de police + Zoom + tentatives + Connecté + Connexion + Détails + Téléchargé + Erreur + Erreur de reconnexion des serveurs + Erreurs + Fichiers + Réception de message + Messages reçus + Messages envoyés + Pas d\'information, essayez de recharger + En attente + Routé via un proxy + Messages reçus + Total reçu + Erreurs de réception + Reconnecter + Reconnecter tous les serveurs + Réinitialiser toutes les statistiques + Réinitialiser toutes les statistiques ? + Envoyé directement + Total envoyé + Envoyé via un proxy + Infos serveurs + Afficher les informations pour + À partir de %s. + À partir de %s. \nToutes les données restent confinées dans votre appareil. + Statistiques + Total + Serveur XFTP + Erreurs d\'accusé de réception + Chunks téléversés + Connexions + Créées + Erreurs de suppression + doublons + expiré + Ouvrir les paramètres du serveur + autre + autres erreurs + Sécurisées + Erreurs d\'envoi + Taille + Inscriptions + Erreurs d\'inscription + Inscriptions ignorées + Fichiers téléversés + Modéré + Flouter les médias + L\'adresse du serveur de destination %1$s est incompatible avec les paramètres du serveur de redirection %2$s. + La version du serveur de destination %1$s est incompatible avec le serveur de redirection %2$s. + Le serveur de redirection %1$s n\'a pas réussi à se connecter au serveur de destination %2$s. Veuillez réessayer plus tard. + L\'adresse du serveur de redirection est incompatible avec les paramètres du réseau : %1$s. + La version du serveur de redirection est incompatible avec les paramètres du réseau : %1$s. + Fort + Erreur de connexion au serveur de redirection %1$s. Veuillez réessayer plus tard. + Off + Léger + Paramètres + se connecter + message + ouvrir + Confirmer la suppression du contact ? + Le contact sera supprimé - il n\'est pas possible de revenir en arrière ! + Supprimer sans notification + Garder la conversation + Ne supprimer que la conversation + rechercher + vidéo + Contact supprimé ! + Vous pouvez envoyer des messages à %1$s à partir des contacts archivés. + Contacts archivés + Pas de contacts filtrés + Coller le lien + Vos contacts + Barre d\'app accessible + Le contact est supprimé. + Les appels ne sont pas autorisés ! + Vous devez autoriser votre contact à appeler pour pouvoir l\'appeler. + Impossible d\'envoyer un message à ce membre du groupe + Vous pouvez toujours consulter la conversation avec %1$s dans la liste des conversation. + Autoriser les appels ? + appeler + Impossible d\'appeler le contact + Connexion au contact, veuillez patienter ou vérifier plus tard ! + Impossible d\'appeler ce membre du groupe + Conversation supprimée ! + Inviter + Veuillez demander à votre contact d\'autoriser les appels. + Envoyer un message pour activer les appels. + Archiver les contacts pour discuter plus tard. + Flouter pour une meilleure confidentialité. + Connectez-vous à vos amis plus rapidement. + État de la connexion et des serveurs. + Exportation de la base de données des discussions + Poursuivre + Les messages seront marqués comme étant à supprimer. Le(s) destinataire(s) pourra(ont) révéler ces messages. + Supprimer %d messages de membres ? + Message + Augmenter la taille de la police. + Créer + Rien n\'est sélectionné + Lien invalide + Nouveau message + Nouvelles options de médias + Inviter + Nouvelle expérience de discussion 🎉 + Téléchargez les nouvelles versions depuis GitHub. + Il protège votre adresse IP et vos connexions. + Maîtrisez votre réseau + Supprimez jusqu\'à 20 messages à la fois. + Serveurs de fichiers et de médias + Serveurs de messages + Veuillez vérifier que le lien SimpleX est exact. + Les messages seront supprimés pour tous les membres. + %d sélectionné(s) + Aperçu depuis la liste de conversation. + Les messages seront marqués comme modérés pour tous les membres. + Afficher la liste des conversations : + Vous pouvez choisir de le modifier dans les paramètres d\'apparence. + Rétablir tous les conseils + Mise à jour automatique de l\'app + Barre d\'outils accessible + Utiliser l\'application d\'une main. + Choisir + proxy SOCKS + Vous pouvez enregistrer l\'archive exportée. + Sauvegarder et se reconnecter + Connexion TCP + Certains fichiers n\'ont pas été exportés + Vous pouvez migrer la base de données exportée. + %1$d erreur(s) de fichier :\n%2$s + %1$d autre(s) erreur(s) de fichier. + Erreur lors du transfert de messages + %1$d fichier(s) est(sont) en cours de téléchargement. + %1$s messages non transférés + Télécharger + Transfert de messages… + Les messages ont été supprimés après avoir été sélectionnés. + Erreur lors du changement de profil + Sélectionner un profil de discussion + Partager le profil + Votre connexion a été déplacée vers %s mais une erreur inattendue s\'est produite lors de la redirection vers le profil. + Ne pas utiliser d\'identifiants avec le proxy. + Erreur lors de l\'enregistrement du proxy + Mot de passe + Authentification proxy + Utilisez des identifiants de proxy différents pour chaque connexion. + Vos informations d\'identification peuvent être envoyées non chiffrées. + Le téléchargement de %1$d fichier(s) a échoué. + %1$d fichier(s) a(ont) été supprimé(s). + Sécurité accrue ✅ + Une meilleure expérience pour l\'utilisateur + %1$d fichier(s) n\'a (n\'ont) pas été téléchargé(s). + Session de l\'app + Meilleures dates de messages. + Transférer %1$s message(s) ? + Transfert de %1$s messages + Assurez-vous que la configuration du proxy est correcte. + Transférer les messages sans les fichiers ? + De nouveaux identifiants SOCKS seront utilisés chaque fois que vous démarrerez l\'application. + Rien à transférer ! + Ouvrez Safari Paramètres / Sites web / Microphone, puis choisissez Autoriser pour localhost. + Sauvegarde de %1$s messages + L\'archive de la base de données envoyée sera définitivement supprimée des serveurs. + Utilisez des identifiants de proxy différents pour chaque profil. + Utiliser des identifiants aléatoires + Nom d\'utilisateur + Les messages seront supprimés - il n\'est pas possible de revenir en arrière ! + BASE DE DONNÉES DU CHAT + Mode système + Serveur + De nouveaux identifiants SOCKS seront utilisées pour chaque serveur. + Erreur lors de l\'initialisation de WebView. Assurez-vous que WebView est installé et que l\'architecture supportée est arm64.\nErreur : %s + Son muet + Coin + Forme du message + Queue + Cliquez sur le bouton info près du champ d\'adresse pour autoriser l\'utilisation du microphone. + Pour passer des appels, autorisez l\'utilisation de votre microphone. Mettez fin à l\'appel et essayez d\'appeler à nouveau. + Supprimer l\'archive ? + Appels améliorés + Forme des messages personnalisable. + Supprimer ou modérer jusqu\'à 200 messages. + Transférez jusqu\'à 20 messages à la fois. + Protocoles SimpleX audité par Trail of Bits. + Passer de l\'audio à la vidéo pendant l\'appel. + Changer de profil de chat pour les invitations à usage unique. + rapport archivé + Ajoutez les membres de votre équipe aux conversations. + L\'application tourne toujours en arrière-plan + rapport archivé par %s + Autre raison + Archive + 1 rapport + Paramètres de l\'adresse + Barres d\'outils de l\'application + invitation acceptée + Archiver le signalement + Demander + Ajouter à la liste + Toutes les discussions seront supprimées de la liste %s, et la liste sera supprimée + Ajouter des membres à l\'équipe + Conditions acceptées + Ajouter des amis + Accepter les conditions + Ajouter une liste + Tous + À propos des opérateurs + Archiver le signalement ? + Adresse professionnelle + Discussions professionnelles + Flouter + Conditions acceptées le : %s. + Violation des directives communautaires + Conditions d\'utilisation + Créer un lien unique + %s.]]> + Les conditions seront acceptées le : %s. + %s, acceptez les conditions d\'utilisation.]]> + chiffrés de bout en bout, avec une sécurité post-quantique dans les messages directs.]]> + Réception des messages toutes les 10 minutes + %s.]]> + %s.]]> + %s.]]> + La discussion existe déjà ! + La connexion est bloquée par l\'opérateur du serveur :\n%1$s. + Les conditions seront automatiquement acceptées pour les opérateurs activés le : %s. + Le contenu viole les conditions d\'utilisation + Supprimer le rapport + La connexion n\'est pas prête. + Connexion bloquée + %s.]]> + Contacts + Continuer + Créer une liste + Supprimer + Supprimer la liste ? + Supprimer la discussion + Discussions + %s.]]> + %s.]]> + Le texte sur les conditions actuelles n\'a pas pu être chargé. Vous pouvez consulter les conditions en cliquant sur ce lien : + Les messages directs entre membres sont interdits. + %1$s.]]> + Supprimer la discussion ? + Ajout de serveurs de médias et de fichiers + Ajout de serveurs de messages + %s.]]> + Appareils Xiaomi : veuillez activer le démarrage automatique dans les paramètres du système pour que les notifications fonctionnent.]]> + La discussion sera supprimé pour tous les membres - cela ne peut pas être annulé ! + Le discussion sera supprimé pour vous - il n\'est pas possible de revenir en arrière ! + Les conditions seront acceptées pour les opérateurs activés après 30 jours. + La connexion nécessite une renégociation du chiffrement. + avec un seul contact - partagez en personne ou via n\'importe quelle messagerie.]]> + Adresse ou lien unique ? + Sécurité des connexions + Professionnels + Le fichier est bloqué par l\'opérateur du serveur :\n%1$s. + Favoris + %d rapports + modérateur(trice) + Décentralisation du réseau + Seuls vous et les modérateurs le voient + Seuls l\'expéditeur et les modérateurs le voient + Erreur d\'enregistrement des serveurs + Pas de serveurs pour recevoir des messages. + Pas de message + Pour les réseaux sociaux + Vérifier plus tard + Serveurs prédéfinis + Signaler autre : seuls les modérateurs du groupe le verront. + Activer les journaux (logs) + Notifications et batterie + demande à se connecter + Pas de serveurs de médias et de fichiers. + Pas de serveurs de messages. + Pas de serveurs pour recevoir des fichiers. + Spam + Erreur d\'enregistrement des paramètres + Erreur lors de la création du rapport + Les adresses SimpleX et les liens à usage unique peuvent être partagés en toute sécurité via n\'importe quelle messagerie. + Spam + Signaler + Pas de discussions non lues + Groupes + Signalements + Signaler le profil d\'un membre : seuls les modérateurs du groupe le verront. + Signaler le spam : seuls les modérateurs du groupe le verront. + Ouvrir le lien + Ouvrir des liens depuis la liste de discussion + Erreur de mise à jour du serveur + Les serveurs pour les nouveaux fichiers de votre profil de discussion actuel + Serveur de l\'opérateur + Le protocole du serveur a été modifié. + Activer Flux + Ce message a été supprimé ou n\'a pas encore été reçu. + Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement. + Partager publiquement votre adresse + Par exemple, si votre contact reçoit des messages via un serveur SimpleX Chat, votre application les transmettra via un serveur Flux. + Seuls les propriétaires peuvent modifier les préférences. + Le rôle deviendra %s. Toutes les personnes présentes dans le discussion en seront informées. + Erreur lors de la création d\'une liste de discussion + Erreur de chargement des listes de discussion + Erreur de mise à jour de la liste des discussions + Pas de discussions + Pas de discussions trouvées + Ouvrir avec %s + Sauvegarder la liste + Modifier + Partagez votre adresse SimpleX sur les réseaux sociaux. + Pas de service d\'arrière-plan + Barre de discussion accessible + Vérifier les conditions + Opérateur + Serveurs %s + Ou importer un fichier d\'archive + Ouvrir les conditions + Pour le routage privé + Pas de serveurs pour le routage privé des messages. + Erreur lors de la validation des conditions + Erreurs dans la configuration des serveurs. + Pour le profil de discussion %s : + Pas de serveurs pour envoyer des fichiers. + Veuillez réduire la taille du message et envoyer le à nouveau. + Veuillez réduire la taille du message ou supprimer le média et renvoyer le message. + pour une meilleure protection des métadonnées. + - Premier message non lu à l\'ouverture.\n- Sauter aux messages cités. + Respect de la vie privée de vos clients. + Le deuxième opérateur prédéfini de l\'application ! + Erreur lors de l\'ajout du serveur + Erreur d\'enregistrement de la base de données + Signaler un contenu : seuls les modérateurs du groupe le verront. + Opérateurs de serveur + Pas de discussions dans la liste %s. + L\'opérateur du serveur a changé. + Ouvrir le lien web ? + Signaler une infraction : seuls les modérateurs du groupe le verront. + La connexion a atteint la limite des messages non délivrés, votre contact est peut-être hors ligne. + Serveur ajouté à l\'opérateur %s. + L\'application protège votre vie privée en utilisant des opérateurs différents pour chaque conversation. + Sélectionnez les opérateurs de réseau à utiliser. + SimpleX Chat et Flux ont conclu un accord pour inclure les serveurs exploités par Flux dans l\'application. + Notes + Ou à partager en privé + Adresse SimpleX ou lien unique ? + Renégociation du chiffrement en cours. + Réparer + Réparer la connexion ? + Partager un lien unique avec un ami + Pour vous protéger contre le remplacement de votre lien, vous pouvez comparer les codes de sécurité des contacts. + Comment il contribue à la protection de la vie privée + Motif du signalement ? + Le rapport sera archivé pour vous. + Contenu inapproprié + Nouveau serveur + Vous pouvez configurer les serveurs via les paramètres. + Pour envoyer + Profil inapproprié + Site web + Liste + Signalement des membres + Modifier l\'ordre + Mise à jour + Les messages directs entre membres sont interdits dans cette discussion. + Non + Oui + Opérateur de réseau + Utiliser pour les fichiers + Utiliser pour les messages + Voir les conditions + Opérateurs de réseau + Inviter à discuter + Le nom de liste et l\'emoji doivent être différents pour toutes les listes. + Nom de la liste... + Quitter la discussion ? + Vous ne recevrez plus de messages de cette discussion. L\'historique sera préservé. + Le membre sera retiré de la discussion - cela ne peut pas être annulé ! + Votre profil de discussion sera envoyé aux autres membres + Vos serveurs + Utiliser %s + Utiliser les serveurs + Pour recevoir + Ouvrir les modifications + Messages non distribués + Vous pouvez copier et réduire la taille du message pour l\'envoyer. + Quitter la discussion + Voir les conditions mises à jour + Navigation améliorée dans les discussions + Lorsque plusieurs opérateurs sont activés, aucun d\'entre eux ne dispose de métadonnées permettant de savoir qui communique avec qui. + Le message est trop volumineux ! + Mobiles à distance + Modifier la liste + Vous pouvez définir les opérateurs dans les paramètres Réseau et serveurs. + Transparence + Vous pouvez définir un nom de connexion pour vous rappeler avec qui le lien a été partagé. + Autoriser le signalement des messages aux modérateurs. + 1 an + Tous les signalements seront archivés pour vous. + Archiver tous les signalements ? + Archiver %d signalements ? + Modifier la suppression automatique des messages ? + Supprimer les messages de discussion de votre appareil. + par défaut (%s) + Bloquer ces membres pour tous ? + Désactiver la suppression automatique des messages ? + Désactiver la suppression des messages + Ne manquez pas les messages importants. + Recevoir une notification en cas de mention. + Vie privée et sécurité renforcées + Suppression plus rapide des groupes. + Envoi plus rapide des messages. + Pour moi + Archiver les rapports + Pour tous les modérateurs + Les membres peuvent signaler des messages aux modérateurs. + Groupes plus performants + Erreur lors de la lecture de la phase secrète de la base de données + Aider les administrateurs à modérer leurs groupes. + Tous les nouveaux messages de ces membres seront cachés ! + 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 @@ गुप्त स्वीकार करें पूर्वनिर्धारित सर्वर जोड़ें प्रोफ़ाइल जोड़ें - सर्वर जोड़े… + सर्वर जोड़े हमेशा बने रहें संलग्न करना उन्नत संजाल समायोजन @@ -69,7 +69,6 @@ स्वागत %1$s! शुरुआत भेजना - रंग बचाओ साझा करना अस्वीकार आवश्यक @@ -118,7 +117,6 @@ नेटवर्क की स्थिति नया संपर्क अनुरोध सभी फाइलों को मिटा दें - संग्रह हटा देना नया डेटाबेस संग्रह नए सदस्य की भूमिका अधिसूचना सेवा @@ -130,7 +128,6 @@ अधिसूचना पूर्वावलोकन सूचनाएं सभी के लिए हटाएं - लिखचीत संग्रह हटा दे\? चैट प्रोफ़ाइल हटाएं\? चैट प्रोफ़ाइल हटाएं\? के लिए चैट प्रोफ़ाइल हटाएं @@ -219,9 +216,7 @@ कॉल समाप्त कॉल चल रहा है छवियों को स्वत: स्वीकार करें - चैट संग्रह चैट रोक दी गई है - चैट संग्रह आप इस समूह से संदेश प्राप्त करना बंद कर देंगे। चैट इतिहास संरक्षित किया जाएगा। %s की भूमिका को %s में बदला पूर्ण 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 @@ - Pretraži - Isključeno - \ No newline at end of file + Pretraži + Isključeno + Prekini + Prosledi do 20 poruka odjednom. + Sakriti kontakt i poruku + %1$d greska(e) datoteka:\n%2$s + %1$d datoteka se i dalje preuzima. + %1$d datoteka neuspešno preuzeta. + %1$d odstranjene datoteke. + %1$d datoteka nije preuzeta. + završen + %1$s želi da se poveže sa tobom putem + Prihvatiti + Pozivnica za grupu je istekla + Grupni link + Sakriti + Dobar dan! + Istorija nije poslana novim članovima. + Sakriti + 5 minuta + Ceo Link + Proslediti poruke… + Prekini menjanje adrese? + Ako se ne možeš sastati, pokaži QR kod u video pozivu ili podeli link. + Sakriti profil + a + b + prihvati poziv + Dodeliti dozvolu + Slušalice + POMOĆ + Grupa će biti obrisana za Vas – ovo ne može da se poništi! + Akcenat + Grupni linkovi + Madjarski i Turski UI + Kako koristiti markdown + Iz Galerije + 1 izveštaj + O SimpleX adresama + iznad, onda: + Prihvati zahtev za vezu? + Zdravo!\nPoveži se sa mnom pomoću SimpleX Razgovora: %s + Grupa neaktivna + U potpunosti decentralizovan - vidljivo samo za članove grupe. + Celo ime grupe: + pomoć + Celo Ime: + Prekini poziv + Prihvatiti + Francuski interfejs + Sakriti ekran aplikacije u nedavnim aplikacijama. + sati + 30 sekundi + Sakriti: + Sakriti + Prosledi poruke bez datoteka? + Kako da + Prihvatiti + %1$d poruke preskočene. + %1$d preskočena poruka(e) + 1 mesec + 1 nedelja + Pozivnica za grupu više nije validna. Pošiljalac je uklonio. + Grupa nije pronadjena! + Prihvati uslove + Grupa već postoji! + Prekini razgovor + Istorija + Dodeliti dozvolu(e) za uspostavljanje poziva + Kako radi + %1$s ČLANOVI + Upiši ime grupe: + Kako koristiti svoje servere + ICE serveri (jedan po liniji) + Prekini menjanje adrese + Nadji Radnu Površinu + Skriveno + %1$s poruke nisu prosledjene + Grupe + Kako se koristi + O SimpleX Razgovoru + %1$d neuspešno u dešifrovanje poruke. + grupni profil ažuriran + grupa odstranjena + Grupa + Ako izabereš da odbiješ pošiljalac NEĆE biti obavešten. + Skriveni razgovori + Grupa će biti obrisana za sve članove – ovo se ne može poništiti! + Domaćin + Kako utiče na bateriju + Kako pomaže privatnosti + Kako SimpleX radi + Grupni profil je uskladnjen na uredjajima korisnika, ne na serverima. + O operatorima + Skrivena šifra profila + 1 dan + Upiši ispravnu pristupnu frazu. + 1 minut + Prihvatiti + Dobro jutro! + Dodatno smanjena potrošnja baterije. + O SimpleX + prihvaćena pozivnica + poruka + Odstraniti i obavestiti kontakte + Odstraniti kontakt? + Odstraniti bazu podataka sa ovog uređaja + Odstraniti kontakt + Greška + Odstranjeno u + Šifra + Ažuriranje dostupno: %s + Napravi adresu + odstranjen kontakt + Dodaj poruku dobrodošlice + Poslano u: %s + Ažuriraj + Šifra profila + Administratori mogu da blokiraju. + Greška + Napravi jednokratnu poveznicu + Nalepiti + PODEŠAVANJE + Profilne slike + Razumeo + Odstranjeno + odstranjeno + Napraviti + PORUKE I DATOTEKE + Poruka + SERVERI + Odstraniti profil razgovora + administratori + Nasumično + greška + Greška: %1$s + Odstraniti %d poruke? + Odstraniti + Podešavanje + Ažuriraj + Odstraniti sve datoteke + Odstraniti profil razgovora? + Poruke + administrator + Odstranjeno u: %s + Oblikuj profilne slike + Aktivne veze + Dodaj na drugi uredjaj + Greška + Greška + Adresa + Greške pri potvrdi + Lista + Ime liste... + Odstraniti + Odstraniti nakon + Dodaj adresu na svoj profil da bi tvoji kontakti mogli da dele sa ostalima. Ažuriranje profila biće poslano tvojim kontaktima. + Dodaj kontakt + Dodaj listu + Prikazati šifru + Odstraniti + Promena adrese će biti prekinuta. Biće upotrebljena stara adresa. + Poslano direktno + Sve + Poslano u + poslano + Odstraniti profil razgovora za + Prihvati anonimno + Podeliti + Dodaj na listu + Pristup na servere pomoću SOCKS proxy na portu %d? Proxy mora da bude uključen pre omogućavanja ove opcije. + Prihvaćeni uslovi + Dodaj server + Dodaj unapred postavljene servere + Odstraniti adresu? + odstranjena grupa + Dodaj nalog + Odstraniti profil razgovora? + Napraviti + Odstraniti razgovor? + Odstranjena adresa + Odstranjena baza podataka + Arhiviraj i otpremi + Odstraniti razgovor + Ime profila: + Dodaj server skeniranjem QR koda. + Ažuriraj + Dodatni akcent + Odstraniti + Tema profila + Dodat serveri za poruke + Dodatni akcent 2 + Podešavanje + Podešavanje adrese + Dodaj tvoje članove tima u konverzaciju. + Dodaj prijatelja + Dodaj člana tima + Arhiva + poveži + Kamera + Više + Otključaj + Nezaštićeno + Nepoznati serveri + Bluetooth + Pozivi na zaključanom ekranu: + %d dana + Poveži automatski + Povezano + Greške + Preuzeto + povezivanje… + Kamera + %d mesec + Datoteka nije pronađena + %d poruke blokirane administratorom + %d izvještaji + poziv + Kamera nije dostupna + pozivanje… + %d min + %d sec + Preuzeti datoteke + blokirano administratorom + Vi + nepoznat status + Poveži se anonimno + greška u pozivu + Poveži + nepoznat format poruke + Povezano + %ds + %d sata(i) + Povezivanje + povezivanje… + Poveži + Poveži + povezano + povezano + Preuzimanje + Napredna podešavanja + Poziv u toku + POZIVI + Blokiraj članove grupe + Nepoznati serveri! + Datoteka + Datoteka + Uključiti zvuk + Kamera i mikrofon + vi: %1$s + Povezano sa telefonom + blokirano + %d poruka blokirano + povezivanje + Povezan telefon + VI + Zamućeno za bolju privatnost. + %d meseca(i) + Poziv završen + Poništiti + 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. + povezivanje… + Uključiti zvuk + povezano + Povezano + Preuzimanje + Povezati direktno? + %d dan + %dh + %dm + %dmon + povezivanje… + nepročitano + poziv završen %1$s + poziv u toku + povezano direktno + uključeno + %d sat + podebljano + blokirano + Napredna podešavanja + blokirano %s + Blokiraj za sve + povezano + Poziv već završen! + Nepoznata greška + povezivanje + povezivanje (prihvaćeno) + Blokiraj člana za sve? + Blokiraj člana + Blokirano administratorom + Blokiraj člana? + Zamućeno + Otkači + %d minut(a) + Proveri ažuriranje + Stabilno + DATOTEKE + %s otpremljeno + Onemogućiti obavještenja + %s nije verifikovan + Sve poruke biće odstranjene - ovo ne može biti vraćeno! + Profil Chata + Onemogućiti + %d nedelja + - neobavezno obavestiti o obrisanim kontaktima.\n- imena profila sa razmacima.\n- i još mnogo toga!\" + Uvoženje arhive + Datoteke + Otpremljeno + %s je verifikovan + Konzola chata + Skenirati QR kod + Server + Onemogućiti + BAZA PODATAKA CHATA + onemogućeno + Greška pri uvoženju teme + Datoteke i medijski sadržaji su zabranjeni. + Ili nalepiti link arhive + Onemogućeno za sve grupe + Nestaje u + Datoteke i medijski sadržaji + svi članovi + Poruke koje nestaju + onemogućeno + Dozvoliti pozive? + %d sekunde(i) + Ili skenirati QR kod + Onemogućeno + Aplikacija + RAZGOVORI + Datoteke i medijski sadržaji su zabranjeni! + Poruke koje nestaju su zabranjene u ovom razgovoru. + Chat je zaustavljen + Datoteka sačuvana + Verzija aplikacije + Verzija aplikacije: v%s + Tema razgovora + %s sekunda(i) + Baza podataka Chata izvezena + Operator + Operacioni server + Poruke koje nestaju + Poruke koje nestaju su zabranjene. + Adresa servera + Datoteke i medijski sadržaji nisu dozvoljeni + Dozvoliti + Razgovor sa kreatorima + Razgovori + %d nedelja(e) + Računar + Direktne poruke između članova su zabranjene. + Direktne poruke između članova su zabranjene u ovoj grupi. + %s serveri + Dozvoliti + Direktne poruke + Onemogućeno za sve + Chat migriran! + %s, %s i %d članovi + Prekinuti vezu + Chat već postoji! + Baza podataka Chata odstranjena + Proveri ažuriranje + Chat je zaustavljen. Ako već koristiš ovu bazu podataka na drugom uređaju, trebala bi se vratiti pre nego što se pokrene chat. + Proveri poruke svakih 10 minuta + %s povezan + %s, %s i %s povezani + direktno + Chat + QR kod + Chat je pokrenut + Uvesti bazu podataka + BAZA PODATAKA CHATA + Chat je zaustavljen + %s, %s i %d ostali članovi povezani + Uvoz neuspešan + Razgovor će biti odstranjen za tebe - ovo se ne može vratiti! + Uvesti temu + Baza podataka Chata uvezena + %s (sadašnji) + Nestaje u: %s + Uvesti + Boje Chata + %s: %s + Ili uvesti arhiviranu datoteku + Razgovor će biti odstranjen za sve članove - ovo se ne može vratiti! + Direktne poruke između članova su zabranjene u ovim porukama. + Prekinuti vezu + %s preuzeto + Već ste povezani na %1$s. + Glasovne poruke nisu dozvoljenje + Greška u vezi + Test server + audio poziv (nije e2e šifrovan) + Novi razgovor + Sačuvati i obavestiti kontakt + Sačuvati i obavestiti članove grupe + tajna + video poziv (nije e2e šifrovan) + Video uključen + Audio isključeno + Audio uključeno + Crna + isključeno` + Zahvaljujući korisnicima – doprinesi pomoću Weblate! + Započeti razgovor + italic + Urediti + Sačuvati + Video + Blok + sekunde + Test serveri + Vaša podešavanja + Mikrofon + Ugao + Popraviti + Obezbeđeno + Veze + nova poruka + Email + Koristi SOCKS proxy? + Veza blokirana + Obaveštenja + Video + Prihvatili ste vezu + Očistiti + SimpleX adresa + SimpleX Logo + Prikazati: + UREĐAJ + Nova poruka + Sekundarni + Kontakti + Isključeno + Zahvaljujući korisnicima – doprinesi pomoću Weblate! + Omogućiti + Vaši serveri + Sačuvati + sek + Glasovne poruke + OK + Audio poziv + Video poziv + Koristi server + Adresa vašeg servera + Vaši SMP serveri + Vaši XFTP serveri + Obaveštenja i baterija + Prikazati + e2e šifrovano + Otvoriti + Omogući automatsko brisanje poruka? + šifrovanje ok za %s + Isključiti zvuk + SimpleX + Teme + Glasovne poruke + Audio/video pozivi su zabranjeni. + Otvoriti grupu + Svi profili + Započeti novi razgovor + Očistiti + Očistiti razgovor + Tvoj server + šifrovanje prihvaćeno za %s + Glasovne poruke su zabranjene. + veza %1$d + Video isključen + ništa + Veza + Omiljeno + Kontakti + Omiljen + Nikada + Veza + TEME + Audio/video pozivi + ne + šifrovanje ok + šifrovanje prihvaćeno + Zahvaljujući korisnicima – doprinesi pomoću Weblate! + Nazad + Vaša SimpleX adresa + Sačuvati i obavestiti kontakte + Zahvaljujući korisnicima – doprinesi pomoću Weblate! + uređeno + Ne + Ne + Zadržite konverzaciju + SimpleX Adresa + Sačuvati + simplexmq: v%s (%2s) + EKSPERIMENTALNO + nikada + Očistiti + Zahvaljujući korisnicima – doprinesi pomoću Weblate! + e2e šifrovan zvučni poziv + video poziv + Koristi %s + Koristi servere + Vaši kontakti + Ne + Započeti razgovor? + otvoriti + Nastaviti + Otvoriti + Pozadina + audio poziv + Audio & video pozivi + Nastaviti + Audio i video pozivi + Urediti + isključeno + Glasovne poruke su zabranjene u ovom razgovoru. + Isključiti zvuk + Zadržati + Popraviti + Očistiti razgovor? + Očistiti privatne beleške? + Nastaviti + arhivirani izveštaj + Uspešno instalirano + Skenirati telefonom + PING interval + Na čekanju + Poslane poruke + Bezbednostni kod + Poslati pitanja i ideje + Ponoviti uvoz + Trenutna obaveštenja! + Poslati Poruku + Trenutno + Ukloniti člana + Poslati + Sačuvano + Slika + jednokratna veza + Koristi nasumične pristupne podatke + Port + Korisničko ime + port %d + Sačuvati podešavanja? + Privatna obaveštenja + Dolazni audio poziv + bez e2e šifrovanja + Okrenuti kameru + odblokiran %s + uklonjena kontakt adresa + prihvatiti šifrovanje za %s… + uklonjeno + levo + Napustiti grupu + Uloga + Svetlo + %dn + Nevažeća veza + Statistika + Ponovo povezati + Veličina + Odstraniti sliku + propušten poziv + Odbijeni poziv + Poziv na čekanju + Poslana poruka + Anonimne grupe + Izabrati + Pozvati + arhivirani izveštaj od %s + nevažeći razgovor + Opis + Trenutna obaveštenja + Zaustaviti razgovor + Poruka koja nestaje + bez detalja + Skenirati kod + Pristupiti + Pristupiti grupi? + član %1$s promenjen u %2$s + Poruka dobrodošlice + Ukloniti sliku + Privatne beleške + Nevažeća veza + Dobrodošli %1$s! + Periodična obaveštenja + Otvoriti konzolu razgovora + pretraži + video + peer-to-peer + Anonimni režim štiti Vašu privatnost koristeći novi nasumični profil za svaki kontakt. + nedelje + Interna greška + Sačuvano od %s + sačuvano + pozvan + Sačuvana poruka + Obnoviti boje + Povezati telefon + Arhiviraj izveštaj? + Prijem + Izveštaj + U odgovoru na + Izveštaji + Beleške + Poslati živu poruku + Odstraniti veze na čekanju? + Skenirati / Nalepiti link + Neispravan bezbednostni kod! + odbijeni poziv + Pozvati + neaktivan + Sačuvati i ponovo povezati + k + Pristupiti grupi? + nevažeći format poruke + Uključiti + Odstraniti poruku? + uklonjena profilna slika + profilna slika + Tamno + Povezani telefoni + Pretplaćen + Pitati + Pozvati u grupu + Sistem + Sistem + Svetlo + Obnoviti boju + Svetlosni režim + Tamni režim + Pristupiti grupnoj konverzaciji + Adresa računara + Pristupiti Vašoj grupi? + Trenutna obaveštenja su onemogućena! + Zaustaviti datoteku + pristupiti kao %s + Informacije + Odgovoriti + Član neaktivan + SOCKS proxy podešavanje + SOCKS proxy + Propušten poziv + Odstraniti poruke nakon + Odstraniti poruke + levo + bezbednostni kod promenjen + Lokalno ime + (trenutno) + Pogledati uslove + Sistem + Tamno + Tamna tema + Povećati veličinu slova. + Detalji + Obnoviti + Obnoviti statistiku + Sistem + Poslati direktnu poruku za povezivanje + Slika + Na čekanju + SMP serveri + Upišite Vaše ime: + Otvoriti razgovor + pozvan %1$s + prihvatiti šifrovanje… + Slanje pomoću + Odstraniti redosled + Privatne beleške + nevažeći podaci + Zaustaviti + Popraviti vezu + Zaustaviti + poslati direktnu poruku + Priložiti + Sačuvati listu + Samouništenje + Pogrešna adresa računara + Kritična greška + uklonjeno %1$s + član + Početna uloga + posredan (%1$s) + Sačuvano od + Periodična obaveštenja su onemogućena! + Poruka dobrodošlice + Otvoriti pomoću %s + Ukloniti arhivu? + Izabrano %d + Dobrodošli! + Previše video snimaka! + Poslati + Obnoviti + Nevažeća veza! + Izabrati profil razgovora + Skenirati QR kod servera + Napredna mrežna podešavanja + SOCKS PROXY + Anonimni režim + broj PING + Obnoviti statistiku? + Pozvati članove + TCP veza + Informacije o serveru + Popraviti vezu? + Arhivirani kontakti + Arhivirati izveštaj + Skenirati QR kod sa radne površine + Napustiti razgovor? + Dolazni video poziv + Pozvati članove + Tamni režim boja + %dd + Povezane radne površine + Šta je novo + Ukloniti + pokušaji + Anonimno + Odstraniti profil + Video poslan + Sačuvati servere + Sačuvati servere? + Ukloniti člana + bez teksta + Poruka dobrodošlice je preduga + Arhiviraj bazu podataka + Periodično + Ukloniti + ČLAN + Pristupanje grupi + SMP server + Pozvati u razgovor + Odblokirati + Italijanski interfejs + Datoteka: %s + Pristupiti anonimno + Napustiti grupu? + Uređaji + Izabrati + Poslati potvrde + Popraviti vezu? + Napustiti + Napustiti razgovor + Izabrati kontakte + Poslati direktnu poruku + Ukloniti člana? + Ponoviti + Veličina teksta + Videa i datoteke do 1gb + Nekompatibilna verzija + Ponoviti preuzimanje + Zaustaviti razgovor? + Odstraniti vezu + Kreirano u: %s + vlasnici + Dovršiti migraciju na drugom uređaju. + Drugo + Kreirano + Ostale greške + Ponoviti + Šifrovati bazu podataka? + %s i %s + vlasnik + Kopirati + Predati + Ostali XFTP serveri + Napraviti SimpleX adresu + Šifrovati + Kreirano u + Otvoriti uslove + Kontrolisanje vaše mreže + Kopirano + Poruka je prevelika + Otpremiti datoteku + Napraviti datoteku + Napraviti tajnu grupu + kreator + Otkriti + Napraviti listu + Greška pri kopiranju + %s i %s su povezani + Napraviti grupu + Napraviti tajnu grupu + Napokon ih imamo! 🚀 + Dovršiti migraciju + Ništa nije izabrano + Konverzacija odstranjena! + Ostali SMP serveri + Verzija jezgra: v%s + Šifrovati bazu podataka + Odstraniti vezu? + Pretplata ignorisana + Napraviti profil razgovora + Napraviti profil + Greške u pretplati + Pasent + Odstraniti listu? + Za primanje + drugo + Napraviti profil + Vi odlučujete ko se može povezati. + Odstraniti grupu? + Obnoviti + Prebaciti + Beta + Urediti sliku + Mala grupa (max 20) + Primljeno u + otkazano %s + primljena potvrda… + Bolje grupe + Drugi razlog + Arhivirati kontakte za kasniji razgovor. + Primljene poruke + Bolja sigurnost ✅ + Napraviti vezu + Primljena poruka + Dodirnuti za Povezivanje + Dodirnuti dugme + Otvoriti port u firewallu + Primljeno u: %s + Pomoću pretraživača + Podeliti adresu javno + Preskočiti ovu verziju + Podseti kasnije + Povezivanje poziva + Odgovoriti na poziv + Mrežna veza + Decentralizacija mreže + Biznis razgovori + Završeno + Proširiti + Prikazati QR kod + Preuzeti datoteku + Napraviti grupni link + Biznisi + Bolje korisničko iskustvo + Spora funkcija + Urediti profil grupe + Dodirnuti za aktivaciju profila. + Tema aplikacije + Dodirnuti za pristup + Dodirnuti za anonimni pristup + Podeliti adresu + Rep + Bolji pozivi + Biznis adresa + Primljena poruka + završiti + Proširiti selekciju uloga + Otvaranje baze podataka… + Obnoviti + Zvučnik isključen + obojen + Podeli vezu + Spam + Zvučnik + Spam + Razgovori nisu pronađeni + Zvučnik uključen + Bezbednije grupe + Odbiti + Odbiti + Razgovori nisu u listi %s. + Previše slika! + autor + Izvesti temu + Žive poruke + Website + dani + Prosleđivanje %1$s poruka + Kada je dostupno + Proslediti i sačuvati poruke + Prosleđeno + Server za prosleđivanje: %1$s\nGreška: %2$s + Označiti da je pročitano + Server za prosleđivanje : %1$s\nGreška odredišnog servera: %2$s + Verifikovati veze + Primeniti na + Zaustavljanje razgovora + Nalepiti link koji ste primili + Oceniti aplikaciju + Nema nepročitanih razgovora + Izveštaj će biti arhiviran za Vas. + Sačuvati neiskorišćenu pozivnicu? + Glasovna poruka… + Koristiti trenutni profil + Nalepiti link + Nalepiti link za povezivanje! + ŽIVO + Zvezdica na GitHubu + Razmera + Verifikovati bezbednost veze + Trenutni profil + Odstraniti grupu + prosleđeno + Tekst poruke + Prosleđeno od + Učitavanje razgovora… + Proslediti %1$s poruku(e)? + Bezbednost veze + Kada je aplikacija pokrenuta + Proslediti + Velika datoteka! + Primeniti + Poruka će biti odstranjena - ovo se ne može poništiti! + Poruka je prevelika! + Učitavanje datoteke + Glasovna poruka + Glasovna poruka (%1$s) + Naučiti više + Označiti da je verifikovano + Koristiti razgovor + Označiti da nije pročitano + Verifikovati vezu + zahtev za povezivanje + Dodirnuti za skeniranje + Nepoznata greška u bazi podataka: %s + Primljene poruke + duplikati + da + Migracija završena + Da + Omogućiti flux + Greška: %s + Greška u zaustavljanju razgovora + Doprinesite + moderirao %s + Trenutan Pin kod + Čekanje na video + Čekanje na dokument + Neophodno + Uvek + Preuzimanje %s (%s) + Ignorisati + Novi Pin kod + Pin kod aplikacije + Omogućiti pin kod za samouništenje + Pin kod za samouništenje + Isključiti + Moderirano u + Vidljiva istorija + Prekinuta veza iz razloga: %s + Mobilna + Poruke poslane + Za sve + Omogući evidenciju + Odmah + greška u slanju + Odstraniti server + blokirali ste %s + Kvantno otporno šifrovanje + Ne praviti adresu + Omogućiti svim grupama + Ne prikazivati ponovo + minuti + meseci + Pregled + Uneti Pin kod + Sesija aplikacije + Prikazati samo kontakt + nepoznato + Promeniti redosled + promena adrese za %s… + Greška u promeni profila! + Koristiti novi anoniman profil + veza uspostavljena + Promeniti listu + Slušalica + Isključiti? + Ovaj uređaj + Omogućiti samouništenje + Uvek uključeno + Odstraniti izveštaj + Poslana slika + Potvrditi + Još nekoliko stvari + Povezati pomoću jednokratnog linka? + Pin kod + Vi i Vaš kontakt, možete da šaljete poruke koje nestaju. + Zvukovi u pozivu + standardno od kraja-do-kraja šifrovanje + moderator + uvek + Smanjiti upotrebu baterije + Proxied(posrednički) serveri + Novi server + Prikazati procente + Napustiti bez čuvanja + POKRENUTI RAZGOVOR + Odblokirati člana za sve? + Priprema za preuzimanje + Proxied(posredovan) + Greška u dešifrovanju + Prikazati pregled + Migracije: %s + Ova grupa više ne postoji. + promenili ste ulogu %s u %s + posmatrač + Status poruke + Status datoteke + podrazumevano (%s) + Persijski UI + Statistika servera ce biti obnovljena - ovo se ne može poništiti! + Promeniti pin kod + Čekanje na sliku + Čekanje na sliku + Čekanje na video + Za početak novog razgovora + Omogućiti svima + Greška u pokretanju razgovora + uloga promenjena iz %s u %s + Promeniti ulogu grupe? + Odstraniti datoteku + SimpleX Tim + moderirano + Operator servera blokira vezu:\n%1$s. + Promeniti ulogu + Ime ovog uređaja + Dodirnuti za početak novog razgovora + pozvani ste u grupu + Slika sačuvana u Galeriji + vaša uloga je promenjena u %s + promenili ste adresu + Kontakt je odstranjen. + Japanski i Portugalski UI + Pin kod za samouništenje + Pronađite i pristupite grupama + Odstraniti za mene + Greška u dekodiranju + Prekinuta veza + Potvrditi brisanje kontakta? + Greška u promeni profila + Pin kod za samouništenje promenjen! + Pin kod za samouništenje omogućen! + Uvesti bazu podataka razgovora? + Odblokirati člana + Odblokirati člana? + Poslati greške + Promeniti + Čekanje na računar… + Ne dozvoliti + omogućeno + Vaš nasumičan profil + Molimo pokušajte kasnije. + Pošaljite nam email + Moderirano u: %s + Potvrditi otpremanje + Popuniti + Greške u dešifrovanju + Ukupno + Potvrditi unapređenje baze podataka + Pozvani ste u grupu + promenili ste vašu ulogu na %s + Potvrda je onemogućena + Imunitet na spam + duplikat poruke + Da + Greška u bazi podataka + podrazumevano (%s) + 1 godina + uklonili ste %1$s + napustili ste + promenili ste adresu za %s + Odblokirati za sve + Status poruke: %s + Status datoteke: %s + TCP veza тајм-аут + Poslana poruka + Omogućeno za kontakt + Omogućeno za Vas + Vi i Vaš kontakt, možete da šaljete glasovne poruke. + Omogućeno za + Nadograditi aplikaciju automatski + Litvanski UI + Prikazati informacije za + WiFi + Uskoro! + istekao + Vi i Vaš kontakt, možete da obavljate pozive. + Kontakt već postoji + Pogledati bezbednostni kod + Podeliti jednokratnu veza + Proslediti poruku… + Adresa ili jednokratna veza? + Omogućiti pristup kameri + Nevažeći QR kod + Jednokratna pozivnica + Uneti poruku dobrodošlice… + Čuvanje %1$s poruka + Prenosna izolacija + Uneti poruku dobrodošlice… (neobavezno) + Koncept poruke + Neispravan pin kod + čeka se potvrda… + Uneti server ručno + kontakt nema e2e šifrovanje + Kontakt odstranjen! + Nevažeći QR kod + Procena bezbednosti + Uporediti datoteku + Kontakt i sve poruke biće odstranjene - ovo ne može biti vraćeno! + Datoteka će biti odstranjena sa servera. + Opozvati + Poništiti pregled slike + %s u %s + Poništiti živu poruku + Jednokratna pozivnica + Nevažeća adresa server! + Sačuvani WebRTC ICE biće uklonjeni. + Unos pin koda + Prosleđena poruka + Ništa za prosleđivanje + Pregledati kasnije + kontakt ima e2e šifrovanje + Zaključati nakon + Skriven kontakt: + Poništiti pregled datoteke + Omogućiti potvrde? + Omogućiti potvrde za grupe? + Transparentnost + Pregled uslova + Poništiti migraciju + Opozvati datoteku + Opozvati datoteku? + Živa poruka! + Greške u preuzimanju + Sačuvati poruku dobrodošlice? + Nekompatibilno! + Pripremanje otpremanja + Sačuvati šifru profila + WebRTC ICE serveri + Pregled obaveštenja + povezivanje poziva… + Pin kod nije promenjen! + Izvesti bazu podataka + Istekla pozivnica! + pozivnica u grupu %1$s + Nekompatibilna verzija baze podataka + Status mreže + Za slanje + Koncept poruke + Prenosna izolacija + Uneti ime ovog uređaja… + Prenosne sesije + Neuspešno preuzimanje + Zaustaviti deljenje adrese? + Greška u primanju datoteke + Greška u čuvanju datoteke + Podeliti profil + Zaustaviti deljenje + Prikazati poslednje poruke + Odstraniti do 20 poruka odjednom. + %s prekinuta veza]]> + Ovo je Vaša vlastita SimpleX adresa! + Očistiti verifikaciju + Greška u otvaranju pretraživača + Nema razgovora + Povezati se sa %1$s? + želi da se poveze sa Vama! + Prikazati status poruke + Veza nije spremna. + Povežite se brže sa svojim prijateljima. + %s prekinuta veza]]> + Datoteka je odstranjena ill je link neispravan + Ukupno poslano + Za društvene mreže + %s nedostaje]]> + Preuzimanje arhive + Ukupno primljeno + %s je neaktivan]]> + Ime kontakta + Greška pri isporuci poruke + Poruke će biti označene za odstranjivanje. Primaoci će moći da otkriju te poruke. + Odstraniti datoteke i medijski sadržaj? + Primanje datoteke biće zaustavljeno. + Datoteka će biti primljena kada vaš kontakt bude na mreži, molimo sačekajte ili proverite kasnije! + Izabrati datoteku + Migriranje + Migriraj uređaj + Zaustaviti slanje datoteke? + Nije izabran nijedan razgovor + promena adrese… + %s je zauzet]]> + promena adrese… + Do 100 poslednjih poruka se šalje novim članovima. + Bolje poruke + Poruke će biti odstranjene - ovo se ne može poništiti! + Slanje datoteke biće zaustavljeno. + Zaustaviti primanje datoteke? + Greška u datoteci + Odstraniti bez obaveštenja + Greška u otpremanju + Ukupno grešaka + Detaljna statistika + Greška prilikom odstranjivanja + Ponoviti otpremanje + Koristiti sa računara + Povezati sa računarom + Povezano sa računarom + Dozvoliti glasovne poruke? + Promeniti prijemnu adresu? + Reakcije na poruku + Dozvoliti reakcije na poruku. + Reakcije na poruku + Promeniti prijemnu adresu + Nema poruka + Nema istorije + Reakcije na poruku su zabranjene. + Bezbedan redosled + Napraviti redosled + Koristiti SOCKS proxy + Greška pri šifrovanju baze podataka + Koristiti za poruke + Greška pri kreiranju profila! + Greška pri učitavanju detalja + Greška pri čuvanju XFTP servera + Otpremljene datoteke + Otvoriti podešavanja servera + Greška pri odstranjivanju privatnih beleški + Greška pri prosleđivanju poruka + Poruke su odstranjene nakon što ste ih odabrali. + NE koristiti privatno usmeravanje. + Kada je IP skrivena + Izgled + Pozvati prijatelje + Potvrditi šifru + Potvrditi pin kod + Odrediti pin kod + Ako unesete Vaš pin kod za samouništenje prilikom otvaranja aplikacije: + Onemogućiti potvrde? + povezivanje (uvedeno) + Uneti šifru u pretragu + Greška pri učitavanju XFTP servera + Greška pri odstranjivanju grupe + Nije moguće poslati poruku + Poslati poruku koja nestaje + Serveri za poruke + Pročitati više + XFTP server + Greška pri pravljenju adrese + neovlašćeno slanje + Verifikovati bezbednostni kod + Dugme zatvoriti + Greška pri pristupanju grupi + Opcije za programere + Alati za programere + Preference chata + Potvrdite da su mrežna podešavanja za ovaj uređaj ispravna. + Sadržaj krši uslove korišćenja + Nije moguće primiti datoteku + Greška pri kreiranju poruke + Otvoriti podešavanja aplikacije + Omogućiti SimpleX Zaključavanje + Podeliti datoteku… + Prilagođeno vreme + Povezani serveri + Vaš profil će biti poslan kontaktu od koga ste primili ovu vezu. + Pokrenuti periodično + Preskočene poruke + Pin kod postavljen! + Svi podaci u aplikaciji su odstranjeni. + Pin kod aplikacije je zamenjen pin kodom za samouništenje. + POTPORI SIMPLEX CHAT + Oblik poruke + IKONA APLIKACIJE + Pristupna fraza baze podataka + Odrediti pristupnu frazu + Baza podataka će biti šifrovana. + Neispravna pristupna fraza! + Uneti pristupnu frazu… + Uslovi korišćenja + Greška pri dodavanju servera + Primljen odgovor + Uvećanje + Dozvoliti slanje glasovnih poruka Vašim kontaktima. + Dozvoliti glasovne poruke samo ako ih Vaš kontakt dozvoljava. + Dozvoli svojim kontaktima dodavanje reakcija na poruke. + Dozvoliti slanje direktnih poruka članovima. + Zabraniti slanje direktnih poruka članovima. + Dozvoliti slanje glasovnih poruka. + Dozvoliti slanje SimpleX veza. + SimpleX veze su zabranjene. + Napraviti grupu koristeći nasumičan profil. + Koristiti aplikaciju jednom rukom. + Potvrditi mrežna podešavanja + Greška pri čuvanju baze podataka + Poslan odgovor + Isključiti zvuk svima + Greška pri čuvanju proxy + Ili deliti privatno + Greška pri čuvanju korisničke šifre + Vaše preference + Prethodno povezani serveri + Komadi su preuzeti + Onemogućiti automatsko brisanje poruka? + Hvala Vam što ste instalirali SimpleX Chat! + Neki serveri nisu prošli test: + Mrežni operator + Možete pokušati ponovo. + Poslano putem proxy + pozvan za povezivanje + pomoću %1$s + Greška pri učitavanju SMP servera + Greška pri čuvanju SMP servera + Greška pri promeni adrese + Pristupnu frazu je potrebna + Spomenuti članove 👋 + Bolja privatnost i bezbednost + Ne propustiti bitne poruke. + prilagođen + Otkriti pomoću lokalne mreže + Uneti pristupnu frazu + Proverite internet vezu i pokušajte ponovo + Bez Informacija, pokušajte ponovo da učitate + Komadi su odstranjeni + SimpleX Chat pozivi + XFTP serveri + Mreža i serveri + Podesite ICE servere + Koristiti direktnu internet vezu? + Instalirati ažuriranje + Vaši pozivi + Omogućiti zaključavanje + ID Baze podataka + Zapis ažuriran u + Koristiti za datoteke + 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. + Potvrda identiteta + Test servera nije uspeo! + Greška pri izvoženju baze podataka razgovora + Vremensko ograničenje protokola + Greška pri slanju poruke + Greška pri odstranjivanju kontakta + Napraviti jednokratnu pozivnicu + Markdown pomoć + Dozvoliti slanje datoteka i medijskog sadržaji. + Pin kod promenjen! + ID Baze podataka: %d + SimpleX veze + Dodani serveri za medije i datoteke + Vas profil %1$s će biti deljen. + primljen odgovor… + Mrežni operatori + Privatno usmeravanje + Nevažeća putanja datoteke + Moderirati + %1$d poruke moderirane %2$s + Poruke od %s biće prikazane! + Pristupna fraza baze podataka i izvoz + Koristite privatno usmeravanje sa nepoznatim serverima. + Intenzivno + Poruka će biti označena za ostranjivanje. Primaoci će moći da otkriju ovu poruku. + Privatnost i bezbednost + SimpleX Chat poruke + Možete pokušati ponovo. + Greška pri čuvanju podešavanja + Postavi poruku koja će biti prikazana novim članovima! + Greška pri čuvanju servera + Član će biti uklonjen iz razgovora – ovo se ne može poništiti! + ažuriran profil + povezivanje (najavljeno) + Greška u promeni uloge + Prilagođena tema + Pronaći razgovore brže + Vaša privatnost + Bivši član %1$s + Vremensko ograničenje protokola po KB + Blago + Podeliti medije… + NE šaljite poruke direktno, čak i ako vaš ili odredišni serveri ne podržavaju privatno usmeravanje. + Promeniti pin kod za samouništenje + Ažurirati pristupna frazu baze podataka + Za mene + Podeliti poruku… + Arhivirati izveštaje + SimpleX veze nisu dozvoljene + Promeniti automatsko brisanje poruka? + Glasovne poruke su zabranjene! + Ili prikazati ovaj kod + Koristiti za nove konekcije + Vaši ICE serveri + pokretanje… + Otvoriti podešavanje + Za odvijanje poziva, dozvoliti upotrebu mikrofona. Završite razgovor i pokušajte ponovo. + Baza podataka je šifrovana! + Zapis ažuriran u: %s + Član će biti uklonjen iz grupe – ovo se ne može poništiti! + Pozivi zabranjeni! + \nDostupno u v5.1 + SimpleX veze + Odstraniti za sve + Dozvoliti slanje poruka koje nestaju. + Pin kod aplikacije + Za sakrivanje neželjenih poruka. + Greška pri čuvanju podešavanja + Nevažeće ime! + Komadi su otpremljeni + Greške u otpremanju + Uvek koristiti privatno usmeravanje. + Markdown u porukama + Kada je omogućeno više od jednog operatera, nijedan od njih nema metapodatke da nauči ko komunicira sa kim. + Dozvoliti vašim kontaktima da vas zovu. + Prijem poruke + Greška pri izvoženju baze podataka razgovora + Možete učiniti vidljivim vašim SimpleX kontaktima putem Podešavanja. + Greška pri slanju pozivnice + Preskočiti pozivanje članova + Za privatno usmeravanje + Dodatni sekundarni + Pokretanje iz %s. + Vaši ICE serveri + Koristiti nasumičnu pristupnu frazu + Onemogućiti SimpleX Zaključavanje + Ponovo povezati server? + Nema primljenih niti poslanih datoteka + neispravan hash poruke + Neispravan ID poruke + Ponovo povezati servere? + Prikazati kontakt i poruku + Prikazati opcije za programere + Neispravan hash poruke + Ponovo povezati server kako biste prisilili dostavu poruke. To koristi dodatni saobraćaj. + neispravan ID poruke + Konfigurisani XFTP serveri + Upozorenje pri isporuci poruke + Povezati se pomoću linka / QR koda + Već imate chat profil sa istim prikaznim imenom. Molimo vas da odaberete drugo ime. + Greška u autentifikaciji + Unapređena konfiguracija servera + %1$d ostale greška datoteke(a). + Autentifikacija nije dostupna + Konfigurisani SMP serveri + Automatsko prihvatanje + Sačuvati preference? + 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. + odblokirali ste %s + Vi ste posmatrač. + Unapređena privatnost i bezbednost + Migriraj na drugi uređaj pomoću QR koda. + odbijeno + Nova pristupna fraza… + Automatsko prihvatanje zahteva za kontakt + Samo Vaši kontakti mogu slati poruke koje nestaju. + Samo Vaši kontakti mogu slati glasovne poruke. + Vi dozvoljavate + Otvoriti fasciklu baze podataka + Sačuvati i ažurirati grupni profil + Uz smanjenu potrošnju baterije. + (skenirati ili nalepiti iz memorije) + Greška u vezi (AUTH) + Ažurirajte aplikaciju i kontaktirajte programere. + Tokom uvoza došlo je do nekih nefatalnih grešaka: + Sačuvati pristupnu frazu u podešavanjima + Poslali ste grupnu pozivnicu + kvantno otporna e2e šifrovanju + Samo Vaši kontakti mogu upućivati pozive. + Ne slati istoriju novim članovima. + Članovi mogu da šalju glasovne poruke. + Uz smanjenu potrošnju baterije. + Greška pri preuzimanju arhive + Svi razgovori biće uklonjeni sa liste %s, a lista odstranjena + Ovaj QR kod nije link! + Već se povezujete! + Migriraj na drugi uređaj + Otvoriti promene + Decentralizovano + Nema informacija o prijem + Povezati se pomoću linka + Zadržati Vaše konekcije + Migriraj ovde + Povezati se pomoću linka? + Instaliraj SimpleX Chat za terminal + Koristiti web port + Samo Vi možete da upućujete pozive. + Otvoriti lokaciju datoteke + odbijeno + Sačuvati grupni profil + Samo vi možete slati poruke koje nestaju. + Uporedite sigurnosne kodove sa vašim kontaktima. + Automatsko prihvatanje slika + Naslov + Neispravna pristupna fraza baze podataka + Migriraj sa drugog uređaja + Samo vi možete slati glasovne poruke + Samo vi možete stavljati reakcije na poruke. + 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 e3a1f124aa..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,250 +1,246 @@ - %1$d üzenet visszafejtése sikertelen. + Nem sikerült visszafejteni %1$d üzenetet. %1$d üzenet kihagyva. %1$d üzenet kihagyva %1$s TAG 1 hónap 1 hét - 6 új felületi nyelv + 6 új kezelőfelületi nyelv 5 perc 1 perc - A SimpleX azonosítóról - Címváltoztatás megszakítása? + A SimpleX-címről + Megszakítja a cím módosítását? Megszakítás 30 másodperc - Egyszer használatos hivatkozás - %1$s szeretne kapcsolatba lépni önnel ezen keresztül: + Egyszer használható meghívó + %1$s szeretne kapcsolatba lépni Önnel a következőn keresztül: A SimpleX Chat névjegye 1 nap - Címváltoztatás megszakítása - A SimpleX névjegye - Kiemelés + Cím módosításának megszakítása + A SimpleXről + Kiemelőszín fogadott hívás - Hozzáférés a kiszolgálókhoz SOCKS proxy segítségével a %d porton? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. + 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. Elfogadás Elfogadás gombra fent, majd: - Elfogadás inkognítóban - Kapcsolatfelvétel elfogadása? + Elfogadás inkognitóban + Elfogadja a meghívási kérést? Elfogadás Elfogadás - Azonosító hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősök számára. - További kiemelés - hiba a hívásban + 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. + További kiemelőszín + híváshiba Csoporttagok letiltása Hitelesítés - Egy üres csevegési profil jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik. - megszakítva %s + Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik. + %s visszavonva Előre beállított kiszolgálók hozzáadása - A hang- és videóhívások le vannak tiltva. - Külön TCP kapcsolat (és SOCKS bejelentkezési adatok) lesz használva minden ismerős és csoporttag számára. -\nFigyelem: ha sok ismerőse van, az akkumulátor- és adathasználat jelentősen megnövekedhet és néhány kapcsolódási kísérlet sikertelen lehet. - hivatkozás előnézet visszavonása - az alkalmazásban minden csevegési profiljához .]]> + A hívások kezdeményezése le van tiltva ebben a csevegésben. + Az összes partneréhez és csoporttaghoz külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.\nMegjegyzé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.]]> + hivatkozás előnézetének visszavonása + Az összes csevegési profiljához az alkalmazásban külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.]]> Mindkét fél küldhet eltűnő üzeneteket. - 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. - Téves üzenet hash + 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. + Érvénytelen az üzenet hasítóértéke Háttér - Tudnivaló: az üzenet- és fájl átjátszók SOCKS proxy által vannak kapcsolatban. A hívások és URL hivatkozás előnézetek közvetlen kapcsolatot használnak.]]> + Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS-proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének elküldése közvetlen kapcsolatot használnak.]]> Alkalmazásadatok biztonsági mentése - Az adatbázis inicializálása sikertelen - Ismerőseivel kapcsolatban marad. A profil változtatások frissítésre kerülnek az ismerősöknél. - A csevegési profil által (alap beállítás), vagy a kapcsolat által (BÉTA). - Egy új véletlenszerű profil lesz megosztva. - Hangüzenetek küldésének engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. - Az alkalmazás build száma: %s - Hang-/videóhívások + Az adatbázis előkészítése sikertelen + A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. + A csevegési profillal (alapértelmezett), vagy a kapcsolattal (BÉTA). + Egy új, véletlenszerű profil lesz megosztva. + A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. + Az alkalmazás összeállítási száma: %s + Hang- és videóhívások Speciális hálózati beállítások - Hangüzenetek küldésének engedélyezése az ismerősei számára. + A hangüzenetek küldése engedélyezve van a partnerei számára. Hang- és videóhívások Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével). Hívás fogadása - Eltűnő üzenetek engedélyezése az ismerősei számára. + Az eltűnő üzenetek küldésének engedélyezése a partnerei számára. Kapcsolódás folyamatban! Nem lehet fogadni a fájlt Hitelesítés elérhetetlen - Alkalmazás verzió - Üdvözlő üzenet hozzáadása - a(z) %s titkosításának elfogadása… - " -\nElérhető a v5.1-ben" + Az alkalmazás verziója + Üdvözlőüzenet hozzáadása + titkosítás elfogadása %s számára… + \nElérhető az v5.1-es kiadásban Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra) - Javított csoportok - Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az ön számára törlődnek. + Továbbfejlesztett csoportok + 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. Hívás befejeződött HÍVÁSOK - és %d további esemény + és további %d esemény Cím - Csatlakozás folyamatban! + A csatlakozás folyamatban van a csoporthoz! Automatikus elfogadás - A háttérszolgáltatás mindig fut - az értesítések azonnal megjelennek, amint üzenetek vannak. + A háttérszolgáltatás mindig fut – az értesítések megjelennek, amint az üzenetek elérhetővé válnak. Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra) Mindkét fél küldhet hangüzeneteket. Téves üzenet ID - Ismerősök általi üzenetreakciók küldésének engedélyezése. + A reakciók hozzáadása az üzenetekhez engedélyezve van a partnerei számára. A hangüzenetek küldése engedélyezve van. - Üzenetreakciók engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. + A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. Vissza - Kikapcsolható a beállításokban – az értesítések továbbra is megjelenítésre kerülnek amíg az alkalmazás fut.]]> - Az adminok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. + Kikapcsolható a beállításokban – az értesítések továbbra is meg lesznek jelenítve amíg az alkalmazás fut.]]> + Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. Hívások a zárolási képernyőn: titkosítás elfogadása… - Ismerősök meghívása le van tiltva! + Nem lehet meghívni a partnert! téves üzenet ID - Ismerős jelölések automatikus elfogadása - Figyelem: NEM fogja tudni helyreállítani, vagy megváltoztatni a jelmondatot abban az esetben, ha elveszíti.]]> + Meghívási kérések automatikus elfogadása + Megjegyzés: NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]> hívás… - További másodlagos + További másodlagos szín Hozzáadás egy másik eszközhöz - Az üzenetreakciók küldése engedélyezve van. - Fájl előnézet visszavonása - Minden csoporttag kapcsolatban marad. - Több akkumulátort használ! Háttérszolgáltatás mindig fut - az értesítések megjelennek, amint az üzenetek elérhetővé válnak.]]> + A reakciók hozzáadása az üzenetekhez engedélyezve van. + Fájlelőnézet visszavonása + Az összes csoporttag kapcsolatban marad. + Több akkumulátort használ! Az alkalmazás mindig fut a háttérben – az értesítések azonnal megjelennek.]]> Letiltás - admin - Fénykép előnézet visszavonása - A jelkód megadása után minden adat törlésre kerül. + adminisztrátor + Képelőnézet visszavonása + A jelkód megadása után az összes adat törölve lesz. Felkérték a videó fogadására - Tag letiltása - Még néhány dolog - Hitelesítés megszakítva - A fájlok- és a médiatartalom küldése engedélyezve van. - Minden csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza! + Letiltás + Néhány további dolog + Hitelesítés visszavonva + A fájlok- és a médiatartalmak küldése engedélyezve van. + Az összes csevegés és üzenet törölve lesz – ez a művelet nem vonható vissza! hanghívás félkövér - Az alkalmazás jelkód helyettesítésre kerül egy önmegsemmisítő jelkóddal. - Arab, bulgár, finn, héber, thai és ukrán - köszönet a felhasználóknak és a Weblate-nek! - Hangüzenetek engedélyezése? - Mindig használjon átjátszó kiszolgálót + Az alkalmazás-jelkód helyettesítve lesz egy önmegsemmisítő-jelkóddal. + Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek. + Engedélyezi a hangüzeneteket? + Mindig használjon továbbítókiszolgálót mindig A hívás már befejeződött! Engedélyezés - Minden ismerősével kapcsolatban marad. + Az összes partnerével kapcsolatban marad. Élő csevegési üzenet visszavonása - Üzenet végleges törlésének engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. (24 óra) + Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra) Hang- és videóhívások - téves üzenet hash - Mindig bekapcsolva - Az Android Keystore biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat megváltoztatás után - lehetővé téve az értesítések fogadását. - Minden alkalmazásadat törölve. - Legjobb akkumulátoridő. Csak akkor kap értesítést, ha az alkalmazás fut (NINCS háttérszolgáltatás).]]> + érvénytelen az üzenet hasítóértéke + Mindig fut + 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. + Az összes alkalmazásadat törölve. + Legjobb akkumulátoridő. Csak akkor kap értesítéseket, amikor az alkalmazás meg van nyitva. (NINCS háttérszolgáltatás.)]]> Megjelenés - Az akkumulátor optimalizálása aktív, mely kikapcsolja a háttérszolgáltatást és az új üzenetek rendszeres kérését. A beállításokon keresztül újra engedélyezhetők. - Tag letiltása? + 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. + Letiltja a tagot? %1$s hívása befejeződött - Jó akkumulátoridő. A háttérszolgáltatá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.]]> + Jó akkumulátoridő. 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.]]> szerző - Elküldött üzenetek végleges törlésének engedélyezése az ismerősei számára. (24 óra) + Az elküldött üzenetek végleges törlése engedélyezve van a partnerei számára. (24 óra) Mégse - Az alkalmazás csak akkor tud értesítéseket fogadni amikor fut, háttérszolgáltatás nem indul el - Jobb üzenetek - A cím módosítása megszakad. A régi fogadási cím kerül felhasználásra. + Az alkalmazás csak akkor tud értesítéseket fogadni, amikor meg van nyitva. A háttérszolgáltatás nem fog elindulni + Továbbfejlesztett üzenetek + A cím módosítása meg fog szakadni. A régi fogadási cím lesz használva. Engedélyezés - Hibás számítógép-azonosító + Érvénytelen számítógépcím Profil hozzáadása - Csatolás + Mellékelés Alkalmazás jelkód Felkérték a kép fogadására - Fényképező - A Keystore-hoz nem sikerül hozzáférni az adatbázis jelszó mentése végett + Kamera + Nem érhető el a Keystore az adatbázis jelszavának mentéséhez hívás folyamatban - Fotók automatikus elfogadása - Hívások engedélyezése az ismerősei számára. - ALKALMAZÁS IKON + Képek automatikus elfogadása + A hívások kezdeményezése engedélyezve van a partnerei számára. + ALKALMAZÁSIKON Kiszolgáló hozzáadása QR-kód beolvasásával. Az eltűnő üzenetek küldése engedélyezve van. - Eltűnő üzenetek engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. + Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. Hang kikapcsolva - A közvetlen üzenetek küldése a tagok számára engedélyezve van. - Alkalmazás + A közvetlen üzenetek küldése a tagok között engedélyezve van. + ALKALMAZÁS Hívás folyamatban - Mindkét fél küldhet üzenetreakciókat. - Mindkét fél tud hívásokat indítani. + Mindkét fél hozzáadhat az üzenetekhez reakciókat. + Mindkét fél tud hívásokat kezdeményezni. Sikertelen hitelesítés - Minden %s által írt új üzenet elrejtésre kerül! - Alkalmazás verzió: v%s - Hívások engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. - Kiszolgáló hozzáadása… + %s összes új üzenete el lesz rejtve! + Az alkalmazás verziója: v%s + A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. + Kiszolgáló hozzáadása Hang bekapcsolva hanghívás (nem e2e titkosított) letiltva - Adatbázis jelmondat megváltoztatása? + Módosítja az adatbázis jelmondatát? kapcsolódva - Jelkód megváltoztatása - %s szerepkörét megváltoztatta erre: %s - A fogadó cím megváltoztatása - Változtatás + Jelkód módosítása + a következőre módosította %s szerepkörét: „%s” + Fogadási cím módosítása + Módosítás Jelkód megerősítése Jelszó megerősítése - Csoport szerepkör megváltoztatása? - Zárolási mód megváltoztatása + Módosítja a tag szerepkörét? + Zárolási mód módosítása Kapcsolódott kapcsolódott Kapcsolódás kapcsolódott - Csatlakoztatott telefon + Társított hordozható eszköz kapcsolódva - Szerepkör megváltoztatása + Szerepkör módosítása Kapcsolódva - Belépési adatok megerősítése - Megváltoztatja a fogadó címet? - cím megváltoztatva - Önmegsemmisítő mód megváltoztatása - megváltoztatta az ön szerepkörét erre: %s + Hitelesítőadatok megerősítése + Módosítja a fogadási címet? + módosította a címet az Ön számára + Önmegsemmisítő-mód módosítása + a következőre módosította az Ön szerepkörét: „%s” Kapcsolódás - Közvetlen kapcsolódás? + Közvetlenül kapcsolódik? Kapcsolódás - közvetlenül kapcsolódva + közvetlenül kapcsolódott kapcsolat %1$d - az ismerős e2e titkosítással rendelkezik + a partner e2e titkosítással rendelkezik Csoport létrehozása véletlenszerű profillal. - Az ismerős és az összes üzenet törlésre kerül - ez a művelet nem vonható vissza! - Az ismerősök törlésre jelölhetnek üzeneteket ; megtekintheti őket. - Kapcsolódás egyszer használatos hivatkozással? - Kapcsolódás egy hivatkozás / QR-kód által + A partner és az összes üzenet törölve lesz – ez a művelet nem vonható vissza! + A partnerei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat. + Kapcsolódik az egyszer használható meghívóval? + Kapcsolódás egy hivatkozáson vagy QR-kódon keresztül Kapcsolódási hiba (AUTH) - Ismerős neve - Kapcsolódik a kapcsolattartási azonosítón keresztül? - Azonosító létrehozása + Csak név + Kapcsolódik a kapcsolattartási címen keresztül? + Cím létrehozása Másolás Folytatás - Kapcsolódás egy hivatkozáson keresztül? - Létező ismerős + Kapcsolódik egy hivatkozáson keresztül? + A partner már létezik Fő verzió: v%s - Ismerős ellenőrizve - Kapcsolódás saját magához? + Partner ellenőrizve + Kapcsolódik saját magához? Kimásolva a vágólapra - Kapcsolódási kérés elküldve! + Meghívási kérés elküldve! Kapcsolódás a számítógéphez Kapcsolat - Név helyesbítése erre: %s? - Kapcsolat időtúllépés - Kapcsolódás %1$s által? + Helyesbíti a nevet a következőre: %s? + Időtúllépés kapcsolódáskor + Kapcsolódik vele: %1$s? Létrehozás - Ismerős beállításai + Partnerbeállítások Kapcsolat Kapcsolat megszakítva kapcsolat létrehozva - az ismerősnek nincs e2e titkosítása - Ismerős engedélyezi - Ismerős elrejtve: - Kapcsolódás számítógéphez - Környezeti ikon + a partner nem rendelkezik e2e titkosítással + Partner engedélyezi + Rejtett név: + Társítás számítógéppel + Szövegkörnyezeti ikon Kapcsolódás egy hivatkozáson keresztül - Ismerősök + Partnerek Kapcsolódási hiba - Az ismerős még nem kapcsolódott! - - kapcsolódás könyvtár szolgáltatáshoz (BÉTA)! -\n- kézbesítési jelentések (20 tagig). -\n- gyorsabb és stabilabb. - Hozzájárulás + A partnere még nem kapcsolódott! + - 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. + Közreműködés kapcsolódás (bemutatkozó meghívó) - SimpleX azonosító létrehozása - törölt ismerős - Csoporttag üzenet törlése? + SimpleX-cím létrehozása + törölt partner + Törli a tag üzenetét? A csevegés fut - Egyszer használatos meghívó hivatkozás létrehozása + Egyszer használható meghívó létrehozása Törlés Új üzenetek ellenőrzése 10 percenként, legfeljebb 1 percen keresztül Adatbázis törlése @@ -252,188 +248,184 @@ Csevegési profil Profil létrehozása Társított számítógép - Törölve ekkor: %s - Törölve ekkor: + Törölve: %s + Törölve Kínai és spanyol kezelőfelület - Ismerősök meghívása nem lehetséges! - A csevegés leállt + Nem lehet meghívni a partnereket! + A csevegés megállt Sötét Profil létrehozása törölt csoport - Törlés mindenkinél + Törlés az összes tagnál Hivatkozás létrehozása Csevegési beállítások - Csevegési archívum Profil törlése Jelenlegi jelkód kapcsolódás Új jelmondat megerősítése… kapcsolódás… Csevegési profil törlése - egyedi - hívás kapcsolódik… + egyéni + kapcsolódási hívás… Téma személyre szabása Jelenleg támogatott legnagyobb fájl méret: %1$s. Fájl törlése Hamarosan! - azonosító megváltoztatása erre: %s… + cím módosítása %s számára… Csevegési adatbázis importálva - CSEVEGÉSI ARCHÍVUM Üzenetek törlése Kiürítés Bezárás gomb - A csevegés leállt + A csevegés megállt (jelenlegi) Témák személyre szabása és megosztása. - Csevegési profil törlése? + Törli a csevegési profilt? Titkos csoport létrehozása Kapcsolódva a számítógéphez - ICE kiszolgálók beállítása + ICE-kiszolgálók beállítása Csoport törlése Hitelesítés törlése - szerző + készítő Megerősítés - Törlés nálam - %d üzenet törlése? - Egyedi témák + Csak nálam + Töröl %d üzenetet? + Egyéni témák kapcsolódás (elfogadva) Kiszolgáló címének ellenőrzése és újrapróbálkozás. - Csoport törlése? - Adatbázis frissítés megerősítése + Törli a csoportot? + Adatbázis fejlesztésének megerősítése Saját profil létrehozása - azonosító megváltoztatása… + cím módosítása… kapcsolódás… Hívás kapcsolása - Fájlok és a médiatartalmak törlése? + Törli a fájl- és a médiatartalmakat? befejezett CSEVEGÉSI ADATBÁZIS - Önmegsemmisító jelkód megváltoztatása - Várólista létrehozása + Önmegsemmisítő-jelkód módosítása + Sorba állítás létrehozása színezett kapcsolódás… Sötét téma törölve - Csevegési profil törlése? + Törli a csevegési profilt? Csevegés a fejlesztőkkel - Hivatkozás törlése? + Törli a hivatkozást? kapcsolódás - Személyreszabott idő + Egyéni időköz Kapcsolódás inkognitóban CSEVEGÉSEK Új profil létrehozása a számítógép alkalmazásban. 💻 kapcsolódás (bejelentve) kapcsolódás… Csevegési adatbázis törölve - kapcsolódás (bejelentve) - Csoportos hivatkozás létrehozása + kapcsolódás (bemutatkozva) + Csoporthivatkozás létrehozása Csevegési konzol - Fájlok törlése minden csevegési profilból - Várólista törlése - Ismerős törlése - Létrehozva ekkor: %1$s - címek megváltoztatása… - Csatlakoztatva a mobilhoz + Fájlok törlése az összes csevegési profilból + Sorba állítás törlése + Partner törlése + cím módosítása… + Társítva a hordozható eszközhöz Jelenlegi jelmondat… - Fájl kiválasztás + Fájl kiválasztása Kép törlése Fájl létrehozása Tikos csoport létrehozása - Kiürítés - Ismerős törlése? + Elvetés + Törli a partnert? Kiürítés - Azonosító létrehozása, hogy az emberek kapcsolatba léphessenek önnel. - Biztonsági kódok összehasonlítása az ismerőseiével. - Fájl összehasonlítás + Cím létrehozása, hogy az emberek kapcsolatba léphessenek Önnel. + Biztonsági kódok összehasonlítása a partnerekével. + Fájl-összehasonlítás Csevegések - Üzenet törlése? - Függő kapcsolatfelvételi kérések törlése? + Törli az üzenetet? + Törli a függőben lévő meghívót? Adatbázis titkosítva! - Üzenetek törlése? - Visszatérés a korábbi adatbázis verzióra - Üzenetek törlése - Adatbázis titkosítási jelmondat frissítve lesz. + Kiüríti a csevegést? + Adatbázis visszafejlesztése + Üzenetek kiürítése + Az adatbázis titkosítási jelmondata frissítve lesz. Kapcsolódás automatikusan - Adatbázis hiba - Adatbázis titkosítási jelmondat frissül és eltárolásra kerül a beállításokban. - Adatbázis ID - Adatbázis ID: %d - Adatbázis azonosítók és átviteli izolációs beállítások. - Az adatbázis titkosítás jelmondata megváltoztatásra és mentésre kerül a Keystore-ban. - Az adatbázis titkosításra kerül és a jelmondat eltárolásra a beállításokban. + Adatbázishiba + Az adatbázis titkosítási jelmondata frissülni fog és a beállításokban lesz tárolva. + Adatbázis-azonosító + Adatbázis-azonosító: %d + Adatbázis-azonosítók és átvitel-izolációs beállítások. + Az adatbázis titkosítási jelmondata frissülni fog és a Keystore-ban lesz tárolva. + Az adatbázis titkosítva lesz, a jelmondat pedig a beállításokban lesz tárolva. Kiszolgáló törlése - Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása. + Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár ki van kapcsolva. Letiltás - Letiltás minden csoport számára - Engedélyezés minden csoport részére - engedélyezve ismerős részére - Az eltűnő üzenetek küldése le van tiltva ebben a csoportban. - Azonosító törlése + Letiltás az összes csoport számára + Engedélyezés az összes csoport számára + engedélyezve a partner számára + Az eltűnő üzenetek küldése le van tiltva. + Cím törlése %d hét - Számítógép azonosítója + Számítógép címe %dmp Kézbesítési jelentések! - Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került. + 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. Titkosítás visszafejtési hiba - Eltűnik ekkor: %s + Eltűnik: %s szerkesztve Törlés %d óra %d hónap - Azonosító törlése? - Üzenet kézbesítési jelentések letiltása? - Az adatbázis jelmondat eltérő a Keystore-ba elmentettől. + Törli a címet? + Letiltja a kézbesítési jelentéseket? + Az adatbázis-jelmondat eltér a Keystore-ban lévőtől. Közvetlen üzenetek E-mail - Letiltás mindenki számára + Letiltás Fejlesztői eszközök - Adatbázis jelmondat + Adatbázis-jelmondat %d nap - Szétkapcsolva - Az adatbázis egy véletlenszerű jelmondattal van titkosítva, megváltoztatható. + Kapcsolat bontva + Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat. %dó %dhét Felfedezés helyi hálózaton keresztül - Helyi csoportok felfedezése és csatlakozás - %1$d üzenet moderálva lett %2$s által + Csoportok felfedezése és csatlakozás + %2$s %1$d üzenetet moderált Eltűnő üzenet - Ne hozzon létre azonosítót + Ne hozzon létre címet Ne mutasd újra - SimpleX zárolás kikapcsolása + SimpleX-zár kikapcsolása e2e titkosított ESZKÖZ e2e titkosított videóhívás közvetlen Számítógép %d perc - %d ismerős kiválasztva + %d partner kijelölve Engedélyezés %dhónap - Ebben a csoportban tiltott a tagok közötti közvetlen üzenetek küldése. + A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. %d perc - Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt változtassa meg a jelmondatot. - Üzenet kézbesítés jelentéseket letiltása a csoportok számára? + Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt módosítsa. + Letiltja a kézbesítési jelentéseket a csoportok számára? nap %d nap - Csevegési archívum törlése? - Duplikált megjelenítési név! + Duplikált megjelenítendő név! Letiltás (felülírások megtartásával) Adatbázis fejlesztése %d üzenet letiltva - Eltűnik ekkor: + Eltűnik %d hét - engedélyezve az ön számára + engedélyezve az Ön számára Eltűnő üzenetek Törlés - Törlés és ismerős értesítése + Törlés, és a partner értesítése letiltva - %d másodperc - Minden fájl törlése - Az adatbázis titkosításra kerül. - Adatbázis jelmondat és -exportálás - Az adatbázis titkosításra kerül és a jelmondat eltárolásra a Keystore-ban. - Automatikus üzenet törlés engedélyezése? + %d mp + Az összes fájl törlése + Az adatbázis titkosítva lesz. + Adatbázis-jelmondat és -exportálás + Az adatbázis titkosítva lesz, a jelmondat pedig a Keystore-ban lesz tárolva. + Engedélyezi az automatikus üzenettörlést? Törlés - az adatbázis verziója újabb, mint az alkalmazásé, visszafelé átköltöztetés nem lehetséges a következőhöz: %s + 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 Leírás %d óra %dp @@ -442,7 +434,7 @@ Letiltás (csoport felülírások megtartásával) %d csoportesemény %d hónap - A csoport profiljának szerkesztése + Csoportprofil szerkesztése e2e titkosított hanghívás %d mp Decentralizált @@ -450,205 +442,203 @@ Kép szerkesztése Értesítések letiltása Eszközök - Látható helyi hálózaton + Látható a helyi hálózaton Ne engedélyezze - Archívum törlése - Az eltűnő üzenetek le vannak tiltva ebben a csevegésben. - alap (%s) - duplikálódott üzenet - Számítógép leválasztása? - Számítógép kliens verziója %s nem kompatibilis ezzel az alkalmazással. + Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben. + alapértelmezett (%s) + duplikált üzenet + Leválasztja a számítógépet? + A számítógép-alkalmazás verziója (%s) nem kompatibilis ezzel az alkalmazással. Kézbesítés - %d fájl %s összméretben - Adatbázis jelmondat szükséges a csevegés megnyitásához. + %d fájl, %s összméretben + A csevegés megnyitásához adja meg az adatbázis jelmondatát. %dnap - Engedélyezés mindenki részére - Kézbesítési jelentések kikapcsolva! + Engedélyezés az összes tag számára + A kézbesítési jelentések le vannak tiltva! Kibontás - Hiba az üzenet küldésekor - Jelkód megadása + Hiba történt az üzenet elküldésekor + Adja meg a jelkódot Mindenkinél - Titkosítás újraegyeztetési hiba - Hiba az adatbázis titkosításakor - Hiba a csoport törlésekor + Hiba történt a titkosítás újraegyeztetésekor + Hiba történt az adatbázis titkosításakor + Hiba történt a csoport törlésekor Kilépés mentés nélkül - Tárolt fájlok és médiatartalmak titkosítása - Hiba az azonosító beállításakor - A csoport meghívó lejárt - Hiba az ICE kiszolgálók mentésekor + A tárolt fájlok- és a médiatartalmak titkosítása + Hiba történt a cím beállításakor + A csoportmeghívó lejárt + Hiba történt az ICE-kiszolgálók mentésekor Hiba Hiba - Hiba az XFTP kiszolgálók betöltésekor - Hiba az SMP kiszolgálók betöltésekor - Hiba a hálózat konfigurációjának frissítésekor - TCP életben tartásának engedélyezése - Fényképezőgép megfordítása - Üdv! -\nCsatlakozzon hozzám SimpleX Chat-en keresztül: %s - A megjelenített név nem tartalmazhat szóközöket. + Hiba történt az XFTP-kiszolgálók betöltésekor + Hiba történt az SMP-kiszolgálók betöltésekor + Hiba történt a hálózat konfigurációjának frissítésekor + TCP életben tartása + Kamera váltás + Üdvözlöm!\nCsatlakozzon hozzám a SimpleX Chaten keresztül: %s + A megjelenítendő név nem tartalmazhat szóközöket. Csoport - Üdvözlő üzenetet megadása… (opcionális) - Hiba a csevegési adatbázis exportálásakor - Hiba a fájl mentésekor + Adja meg az üdvözlőüzenetet… (nem kötelező) + Hiba történt a csevegési adatbázis exportálásakor + Hiba történt a fájl mentésekor Helyi fájlok titkosítása - titkosítás egyeztetve %s számára + titkosítás elfogadva %s számára %d üzenet megjelölve törlésre - titkosítás újra egyeztetése engedélyezve + a titkosítás újraegyeztetése engedélyezve van Önmegsemmisítés engedélyezése Olvasatlan és kedvenc csevegésekre való szűrés. - A csevegések betöltése sikertelen + Nem sikerült betölteni a csevegéseket A csoport már létezik! Francia kezelőfelület - Csoport hivatkozások + Csoporthivatkozások Végre, megvannak! 🚀 - Hiba a csevegés elindításakor + Hiba történt a csevegés elindításakor A csoport profilja a tagok eszközein tárolódik, nem a kiszolgálókon. - Jelmondat megadása… - Hiba a felhasználói beállítások frissítésekor + Adja meg a jelmondatot… + Hiba történt a felhasználói adatvédelem frissítésekor Titkosít Csoport nem található! - Hiba az SMP kiszolgálók mentésekor - Visszatérés a korábbi verzióra és a csevegés megnyitása + Hiba történt az SMP-kiszolgálók mentésekor + Visszafejlesztés és a csevegés megnyitása A csoport inaktív Gyors és nem kell várni, amíg a feladó online lesz! - Hiba a csoporthoz való csatlakozáskor + Hiba történt a csoporthoz való csatlakozáskor Kedvenc - Csoport moderáció + Csoport moderálása Fájl - Csoport hivatkozás - titkosítás újraegyeztetés szükséges %s számára - Hiba a profil váltásakor! + Csoporthivatkozás + a titkosítás újraegyeztetése szükséges %s számára + Hiba történt a profilváltáskor! Kísérleti funkciók Engedélyezés (felülírások megtartásával) - Helyes jelmondat bevitele. - A csoport törlésre kerül az ön részére - ez a művelet nem vonható vissza! - Adatbázis titkosítása? + Adja meg a helyes jelmondatot. + A csoport törölve lesz az Ön számára – ez a művelet nem vonható vissza! + Titkosítja az adatbázist? A zárolási képernyőn megjelenő hívások engedélyezése a Beállításokban. - titkosítás egyeztetve - Üzenet kézbesítési jelentések engedélyezése? - Hiba a csoport profil mentésekor + titkosítás elfogadva + Engedélyezi a kézbesítési jelentéseket? + Hiba történt a csoportprofil mentésekor hiba - A fájl törölve lesz a kiszolgálóról. + A fájl törölve lesz a kiszolgálókról. Akkor is, ha le van tiltva a beszélgetésben. - Gyorsabb csatlakozás és megbízhatóbb üzenet kézbesítés. + Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés. Zárolás engedélyezése - SEGÍTSÉG - Teljesen decentralizált - kizárólag tagok számára látható. + SÚGÓ + Teljesen decentralizált – csak a tagok számára látható. Fájl: %s Hívás befejezése - Hiba a csoport hivatkozásának törlésekor + Hiba történt a csoporthivatkozás törlésekor Fájl elmentve Kapcsolat javítása? - Fájlok és médiatartalom + Fájlok és médiatartalmak KONZOLHOZ - Sikertelen titkosítás-újraegyeztetés. - Hiba a felhasználói profil törlésekor + Nem sikerült a titkosítást újraegyeztetni. + Hiba történt a felhasználó-profil törlésekor Csoporttag általi javítás nem támogatott - Üdvözlő üzenetet megadása… + Adja meg az üdvözlőüzenetet… Titkosított adatbázis - Jelszó megadása a keresőben + Adja meg a jelszót a keresőben A fájl akkor érkezik meg, amikor a küldője befejezte annak feltöltését. Fájl letöltése - A csevegés betöltése sikertelen - Kiszolgáló megadása kézzel + Nem sikerült betölteni a csevegést + Adja meg a kiszolgálót kézzel A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! - Hiba a csoport hivatkozásának létrehozásakor - A Galériából + Hiba történt a csoporthivatkozás létrehozásakor + A galériából Engedélyezés (csoport felülírások megtartásával) - Hiba az ismerős törlésekor - A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) - Hiba a szerepkör megváltoztatásakor + Hiba történt a partner törlésekor + A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra) + Hiba történt a szerepkör módosításakor Javítás - A csoport tagjai küldhetnek eltűnő üzeneteket. + A tagok küldhetnek eltűnő üzeneteket. Kapcsolat javítása - Hiba a profil létrehozásakor! - Hiba a tag(-ok) hozzáadásakor + Hiba történt a profil létrehozásakor! + Hiba történt a tag(ok) hozzáadásakor Fájl - A csoport tagjai küldhetnek fájlokat és médiatartalmakat. - Törlés miután - Hiba a beállítás megváltoztatásakor - Hiba a csoport hivatkozás frissítésekor + A tagok küldhetnek fájlokat és médiatartalmakat. + Törlés ennyi idő után + Hiba történt a beállítás módosításakor + Hiba történt a csoporthivatkozás frissítésekor a csoport törölve - csoport profil frissítve - Hiba a függőben lévő ismerős kapcsolatának törlésekor - Hiba a csevegési adatbázis importálásakor - Hiba a kézbesítési jelentések engedélyezésekor! - Hiba az XFTP kiszolgálók mentésekor - A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. - Hiba a tag eltávolításakor + csoportprofil frissítve + Hiba történt a függőben lévő meghívó törlésekor + Hiba történt a csevegési adatbázis importálásakor + Hiba történt a kézbesítési jelentések engedélyezésekor! + Hiba történt az XFTP-kiszolgálók mentésekor + A tagok küldhetnek egymásnak közvetlen üzeneteket. + Hiba történt a tag eltávolításakor befejeződött - Csoport üdvözlő üzenete - Csoport neve: - Hiba a meghívó küldésekor + A csoport üdvözlőüzenete + Adja meg a csoport nevét: + Hiba történt a meghívó elküldésekor Adjon meg egy nevet: - Hiba a felhasználó jelszavának mentésekor + Hiba történt a felhasználó jelszavának mentésekor Téma exportálása - Eszköznév megadása… + Adja meg ennek az eszköznek a nevét… Hiba - A csoport meghívó már nem érvényes, a küldője törölte. + A csoportmeghívó már nem érvényes, a küldője eltávolította. A csoport teljes neve: - segítség - Önmegsemmisítő jelkód engedélyezése + súgó + Önmegsemmisítő-jelkód engedélyezése KÍSÉRLETI - Hiba az azonosító megváltoztatásának megszakításakor - Hiba a fájl fogadásakor + Hiba történt a cím módosításának megszakításakor + Hiba történt a fájl fogadásakor titkosítás rendben - Hiba az ismerős kérelem törlésekor - Üzenet kézbesítési jelentéseket engedélyezése csoportok számára? - Ismerős általi javítás nem támogatott + Hiba történt a meghívási kérés törlésekor + Engedélyezi a kézbesítési jelentéseket a csoportok számára? + Partner általi javítás nem támogatott Fájl nem található Kapcsolat bontása - A csoport tagjai üzenetreakciókat adhatnak hozzá. + A tagok reakciókat adhatnak hozzá az üzenetekhez. Adatbázis exportálása Teljes név: - Tovább csökkentett akkumulátor használat - Hiba a csevegés megállításakor + Tovább csökkentett akkumulátor-használat + Hiba történt a csevegés megállításakor titkosítás rendben %s számára - Csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza! + A csoport törölve lesz az összes tag számára – ez a művelet nem vonható vissza! Titkosítás javítása az adatmentések helyreállítása után. - Hiba a csevegési adatbázis törlésekor + Hiba történt a csevegési adatbázis törlésekor Teljes hivatkozás - Hiba az azonosító megváltoztatásakor - A csoport tagjai küldhetnek hangüzeneteket. - Csoport beállítások + Hiba történt a cím módosításakor + A tagok küldhetnek hangüzeneteket. + Csoportbeállítások Hiba: %s Eltűnő üzenetek - SimpleX zárolás engedélyezése - Hiba a kapcsolat szinkronizálása során - Hiba az azonosító létrehozásakor + SimpleX-zár bekapcsolása + Hiba történt a kapcsolat szinkronizálásakor + Hiba történt a cím létrehozásakor engedélyezve - Hiba a részletek betöltésekor - Hiba történt a kapcsolatfelvételi kérelem elfogadásakor - titkosítás újraegyeztetése engedélyezett %s számára - titkosítás újraegyeztetés szükséges + Hiba történt a részletek betöltésekor + Hiba történt a meghívási kérés elfogadásakor + a titkosítás újraegyeztetése engedélyezve van %s számára + a titkosítás újraegyeztetése szükséges Rejtett csevegési profilok - Fájlok és média - A kép mentve a Galériába - Elrejt + Fájlok és médiatartalmak + A kép elmentve a „Galériába” + Elrejtés Azonnal - A fájlok- és a médiatartalom küldése le van tiltva! + A fájlok- és a médiatartalmak küldése le van tiltva! Profil elrejtése - Kiszolgálók használata + Hogyan használja a saját kiszolgálóit Csevegési üzenetek gyorsabb megtalálása Téma importálása - Hiba a téma importálásakor - Ismerős és üzenet elrejtése - Nem kompatibilis adatbázis verzió + Hiba történt a téma importálásakor + Partner nevének és az üzenet tartalmának elrejtése + Nem kompatibilis adatbázis-verzió Hogyan működik a SimpleX Nem kompatibilis verzió - Elrejt + Elrejtés Bejövő videóhívás Téves jelkód Azonnali - Inkognitó csoportok + Inkognitócsoportok Hogyan - Elrejt + Összecsukás Kép Fejlesztett adatvédelem és biztonság - Figyelmen kívül hagyás + Mellőzés Kép elküldve - Rejtett - Házigazda + Se név, se üzenet + Kiszolgáló Kezdeti szerepkör érvénytelen csevegés óra @@ -657,219 +647,209 @@ Alkalmazás képernyőjének elrejtése a gyakran használt alkalmazások között. Javított kiszolgáló konfiguráció Előzmények - Rejtett profil jelszó + Rejtett profiljelszó Adatbázis importálása Importálás Azonnali értesítések - Inkognitó mód - Csevegési adatbázis importálása? - Azonnali értesítések kikapcsolva! + Inkognitómód + Importálja a csevegési adatbázist? + Az azonnali értesítések le vannak tiltva! Azonnali értesítések! Kép - A fájlok- és a médiatartalom küldése le van tiltva ebben a csoportban. + A fájlok- és a médiatartalmak küldése le van tiltva. Hogyan működik - Elrejt: - Hiba az ismerőssel történő kapcsolat létrehozásában + Elrejtve: + Hiba történt a partnerrel történő kapcsolat létrehozásában ICE-kiszolgálók (soronként egy) - beolvashatja a QR-kódot a videohívásban, vagy az ismerőse megoszthat egy meghívó hivatkozást.]]> - Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen törlődik! - Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás során, vagy ossza meg a hivatkozást. + beolvashatja a QR-kódot a videohívásban, vagy a partnere megoszthat egy meghívási hivatkozást.]]> + Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen el lesz távolítva! + 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. mutassa meg a QR-kódot a videohívásban, vagy ossza meg a hivatkozást.]]> - Megerősítés esetén az üzenetküldő 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. + 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. A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését. - QR kód beolvasásával.]]> - Kapott SimpleX Chat meghívó hivatkozását megnyithatja böngészőjében: - Ha az alkalmazás megnyitásakor az önmegsemmisítő jelkódot megadásra kerül: + QR-kód beolvasásával.]]> + A kapott SimpleX Chat-meghívási hivatkozását megnyithatja a böngészőjében: + Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő-jelkódot: Megtalált számítógép Számítógépek A markdown használata - Csevegő profil létrehozása - Spam és visszaélések elleni védelem - Mobilok leválasztása - Különböző nevek, avatarok és átviteli izoláció. + Csevegési profil létrehozása + Védett a kéretlen tartalommal szemben + Hordozható eszközök leválasztása + Különböző nevek, profilképek és átvitel-izoláció. Elutasítás esetén a feladó NEM kap értesítést. - Szerepkör kiválasztásának bővítése + Szerepkörválasztó kibontása A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! - meghívta - Érvénytelen kapcsolati hivatkozás + meghíva + Érvénytelen kapcsolattartási hivatkozás Némítás nincsenek részletek Nem fogadott hívás Világos - Az üzenet törlésre kerül - ez a művelet nem vonható vissza! - Markdown segítség + Az üzenet törölve lesz – ez a művelet nem vonható vissza! + Markdown súgó új üzenet - Régi adatbázis archívum - Hálózati beállítások + Régi adatbázis-archívum + Speciális beállítások Nincs kézbesítési információ moderált - A tag eltávolítása a csoportból - ez a művelet nem vonható vissza! - Győződjön meg róla, hogy az XFTP-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nem duplikáltak. - Nem kerültek ismerősök kiválasztásra - Nincsenek fogadott, vagy küldött fájlok - Megnyitás mobil alkalmazásban, majd koppintson a Kapcsolódás gombra az alkalmazásban.]]> + A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza! + 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. + Nincs partner kijelölve + Nincsenek fogadott vagy küldött fájlok + Megnyitás hordozható eszköz-alkalmazásban, majd koppintson a Kapcsolódás gombra az alkalmazásban.]]> Markdown az üzenetekben - meghívás a %1$s csoportba + meghívás a(z) %1$s csoportba Zárolási mód - Új mobil eszköz + Új hordozható eszköz Kapcsolatok megtartása Tagok meghívása Üzenetreakciók - Egyszerre csak egy eszköz működhet + Egyszerre csak 1 eszköz működhet Csatlakozik a csoportjához? Nagy fájl! Helyi név Hálózat és kiszolgálók - Értesítés előnézet - Társítsa össze a mobil és az asztali alkalmazásokat! 🔗 + Értesítés előnézete + Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗 közvetett (%1$s) Hamarosan további fejlesztések érkeznek! - Az üzenetreakciók ebben a csevegésben le vannak tiltva. + A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben. Helytelen biztonsági kód! - Ez akkor fordulhat elő, ha ön, vagy az ismerőse régi adatbázis biztonsági mentést használt. - Új asztali alkalmazás! - Most már az adminok is: -\n- törölhetik a tagok üzeneteit. -\n- letilthatnak tagokat (\"megfigyelő\" szerepkör) - meghívta %1$s-t - Ebben a csoportban az üzenetreakciók le vannak tiltva. + Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt. + Új számítógép-alkalmazás! + Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (megfigyelő szerepkör) + meghívta őt: %1$s + A reakciók hozzáadása az üzenetekhez le van tiltva. Nem nincs szöveg TAG - Ez később a beállításokon keresztül módosítható. + Hogyan befolyásolja az akkumulátort Új tag szerepköre - Ki + Kikapcsolva Érvénytelen hivatkozás! - A kapcsolódáshoz Onion kiszolgálókra lesz szükség. - Változások a %s verzióban - Onion kiszolgálók használata, ha azok rendelkezésre állnak. + Újdonságok a(z) %s verzióban Érvénytelen kiszolgálócím! k soha (új)]]> - Győződjön meg arról, hogy az SMP-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. - Onion kiszolgálók nem lesznek használva. + 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. + Az onion-kiszolgálók nem lesznek használva. perc Tudjon meg többet - Új kapcsolattartási kérelem + Új meghívási kérés Csatlakozás a csoporthoz - Összekapcsolt számítógép beállítások - meghívta a csoport hivatkozásán keresztül + Társított számítógép beállítások + meghíva a saját csoporthivatkozásán keresztül elhagyta a csoportot - Összekapcsolt számítógépek + Társított számítógépek Nincs alkalmazás jelkód Némítás, ha inaktív! A meghívó lejárt! (csak a csoporttagok tárolják) Moderálás - be - Japán és Portugál kezelőfelület - Ebben a csoportban az üzenetek végleges törlése le van tiltva. - Onion kiszolgálók nem lesznek használva. - %s eszközzel megszakadt a kapcsolat]]> + bekapcsolva + Japán és portugál kezelőfelület + Az üzenetek végleges törlése le van tiltva. + %s nevű hordozható eszközzel]]> hónap Üzenetvázlat Egy üzenet eltüntetése Végleges üzenettörlés Egyszerre csak 10 videó küldhető el - Csak ön adhat hozzá üzenetreakciókat. + Csak Ön adhat hozzá reakciókat az üzenetekhez. elhagyta a csoportot - Ebben a csevegésben az üzenetek végleges törlése le van tiltva. + Az üzenetek végleges törlése le van tiltva ebben a csevegésben. Max 40 másodperc, azonnal fogadható. - inkognitó a kapcsolattartási azonosító-hivatkozáson keresztül - A kapcsolódáshoz Onion kiszolgálókra lesz szükség. -\nFigyelem: .onion cím nélkül nem fog tudni kapcsolódni a kiszolgálókhoz. + inkognitó a kapcsolattartási címhivatkozáson keresztül + 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. Olasz kezelőfelület Nincsenek háttérhívások Üzenetek - Összekapcsolt mobil eszközök - Lehetővé teszi, hogy egyetlen csevegőprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük. + Társított hordozható eszköz + 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. Az üzenet törlésre lesz jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. - Elhagy + Elhagyás Rendben Nincsenek szűrt csevegések érvénytelen adat - Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nem duplikáltak. - Csak a csoporttulajdonosok engedélyezhetik a fájlok- és a médiatartalmak küldését. - A fájl betöltése - Nincs hozzáadandó ismerős + 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. + Csak a csoport tulajdonosai engedélyezhetik a fájlok- és a médiatartalmak küldését. + Fájl betöltése… + Nincs hozzáadandó partner Üzenetvázlat - meghívta, hogy csatlakozzon - Egyszer használatos meghívó hivatkozás + Függőben lévő meghívó + Egyszer használható meghívó Értesítések Egyszerre csak 10 kép küldhető el - ajánlott %s: %2s + ajánlotta: %s, ekkor: %2s Nem kompatibilis! Tegye priváttá a profilját! Üzenetkézbesítési hiba - Több csevegőprofil - töröltnek jelölve - Elnémítás - Egy mobil összekapcsolása + Több csevegési profil + törlésre jelölve + Némítás + Hordozható eszköz társítása Értesítési szolgáltatás - Csak a csoporttulajdonosok engedélyezhetik a hangüzenetek küldését. - 2 rétegű végponttól-végpontig titkosítással küldött üzeneteket.]]> + Csak a csoport tulajdonosai engedélyezhetik a hangüzenetek küldését. + A felhasználói profilok, partnerek, csoportok és üzenetek csak az eszközön vannak tárolva a kliensen belül. Érvénytelen átköltöztetési visszaigazolás - Csak a csoporttulajdonosok módosíthatják a csoportbeállításokat. + Csak a csoport tulajdonosai módosíthatják a csoportbeállításokat. Nincsenek előzmények Érvénytelen QR-kód Megjelölés olvasottként ÉLŐ - Olvasatlannak jelölve - Több - Bejelentkezés hitelesítő adatokkal - érvénytelen üzenet formátum + Megjelölés olvasatlanként + Továbbiak + Bejelentkezés hitelesítőadatokkal + érvénytelen üzenet-formátum Csatlakozás Az értesítések az alkalmazás elindításáig nem fognak működni - ki` + kikapcsolva` (ez az eszköz: v%s)]]> - ajánlott %s + %s ajánlotta Csoport elhagyása - Minden %s által írt üzenet megjelenik! - Ha a SimpleX Chat-nek nincs felhasználói azonosítója, hogyan lehet mégis üzeneteket küldeni?]]> - Ez akkor fordulhat elő, ha: -\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak. -\n2. Az üzenet visszafejtése sikertelen volt, mert ön, vagy az ismerőse régebbi adatbázis biztonsági mentést használt. -\n3. A kapcsolat sérült. + %s összes üzenete meg fog jelenni! + 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. megfigyelő - inkognitó a csoportos hivatkozáson keresztül - Onion kiszolgálók használata, ha azok rendelkezésre állnak. - Ismerősök meghívása + inkognitó a csoporthivatkozáson keresztül + Onion-kiszolgálók használata, ha azok rendelkezésre állnak. + Barátok meghívása Menük és figyelmeztetések Tagok meghívása csatlakozás mint %s - Nincs kiválasztott csevegés + Nincs csevegés kijelölve Csak helyi profiladatok - inkognitó az egyszer használatos hivatkozáson keresztül - Moderálva lett ekkor: %s - Egyszer használatos meghívó hivatkozás + inkognitó egy egyszer használható meghívón keresztül + Moderálva: %s + Egyszer használható meghívó Érvénytelen név! - Beszélgessünk a SimpleX Chat-ben - Moderálva lett ekkor: + Beszélgessünk a SimpleX Chatben + Moderálva Élő üzenetek - Ellenőrzöttként jelölve - Üzenetkézbesítési bizonylatok! + Hitelesítés + Üzenetkézbesítési jelentések! hivatkozás előnézeti képe - Csoport elhagyása? + Elhagyja a csoportot? nem Hamarosan további fejlesztések érkeznek! - ki + kikapcsolva SimpleX Chat telepítése a terminálhoz - Új megjelenített név: + Új megjelenítendő név: Új jelmondat… nem fogadott hívás Átköltöztetés: %s - Válaszul erre: - Üzenet szövege + Válaszul erre + Név és üzenet Az értesítések csak az alkalmazás bezárásáig érkeznek! Információ ÜZENETEK ÉS FÁJLOK tag Privát kapcsolat létrehozása moderálva lett %s által - 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éma fájl szerkezetére. + Győződjön meg arról, hogy a fájl helyes YAML-szintaxist tartalmaz. Exportálja a témát, hogy legyen egy példa a témafájl szerkezetére. dőlt - Érvénytelen fájl elérési útvonal + Érvénytelen a fájl elérési útvonala Csatlakozik a csoporthoz? nincs e2e titkosítás Új adatbázis-archívum @@ -877,157 +857,153 @@ Meghívás a csoportba Zárolás miután Bejövő hanghívás - Kulcstartó hiba + Kulcstartóhiba Csatlakozik a csoporthoz? - Az inkognitó mód védi személyes adatait azáltal, hogy minden ismerőshöz új véletlenszerű profilt használ. - - stabilabb üzenetkézbesítés. -\n- valamivel jobb csoportok. -\n- és még sok más! + Az inkognitómód úgy védi a személyes adatait, hogy az összes partneréhez új, véletlenszerű profilt használ. + - stabilabb üzenetkézbesítés.\n- picit továbbfejlesztett csoportok.\n- és még sok más! Üzenetreakciók - Nincs társított mobil eszköz + Nincs társított hordozható eszköz Hálózat állapota Új jelkód - Valószínűleg ez az ismerős törölte önnel a kapcsolatot. + Valószínűleg ez a partner törölte Önnel a kapcsolatot. Csatlakozás inkognitóban Csevegés megnyitása elutasított hívás - Rendszeres + Időszakos fogadott, tiltott - Kapcsolódási kérés megismétlése? - Véglegesen csak ön törölhet üzeneteket (ismerőse csak törlésre jelölheti őket ). (24 óra) + Megismétli a meghívási kérést? + Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra) Szerepkör - SimpleX kapcsolattartási azonosító + SimpleX kapcsolattartási cím Megállítás Előre beállított kiszolgáló - Új csevegés kezdése - Nyílt forráskódú protokoll és forráskód – bárki üzemeltethet kiszolgálókat. + Új csevegés indítása + Bárki üzemeltethet kiszolgálókat. Megnyitás - Protokoll időtúllépés - titkos - Előnézet megjelenítése + Protokoll időtúllépése + titok + Értesítés előnézete várakozás a visszaigazolásra… Fájl megállítása - csoport hivatkozáson keresztül - PING időköze + a csoporthivatkozáson keresztül + Időtartam a PING-ek között Eltűnő üzenet küldése - Önmegsemmisítési jelkód - Mentés és csoport profil frissítése + Önmegsemmisítő-jelkód + Mentés és a csoportprofil frissítése Adatvédelem - SimpleX azonosító + Profil SimpleX-címe Jelentse a fejlesztőknek. - Az emberek csak az ön által megosztott hivatkozáson keresztül kapcsolódhatnak. + Ön dönti el, hogy kivel beszélget. Az eltűnő üzenetek küldése le van tiltva. - Csak ön tud hangüzeneteket küldeni. + Csak Ön tud hangüzeneteket küldeni. Frissítés Videó elküldve - Adatbázis jelmondat megváltoztatása + Az adatbázis jelmondatának módosítása Alkalmazás beállítások megnyitása - A jelkód nem változott! + A jelkód nem módosult! Frissítés - Választás - Csak ön tud hívásokat indítani. - Biztonságos várólista + Kijelölés + Csak Ön tud hívásokat indítani. + Biztonságos sorba állítás Értékelje az alkalmazást - Egyszer használatos hivatkozás megosztása - Hiba az adatbázis visszaállításakor + Egyszer használható meghívó megosztása + Hiba történt az adatbázis visszaállításakor %s és %s - Engedélyezve - Csökkentett akkumulátorhasználat - Mentés és ismerősök értesítése + Ön engedélyezi + Csökkentett akkumulátor-használat + Mentés és a partnerek értesítése Előnézet - Csevegés használata + SimpleX Chat használata Megosztás Fogadott üzenet - Üdvözlő üzenet + Üdvözlőüzenet %s, %s és további %d tag kapcsolódott - Csak az ismerőse tud hívást indítani. + Csak a partnere tud hívást indítani. TÉMÁK Túl sok videó! - Csevegési szolgáltatás megállítása az adatbázis műveletek elvégzéséhez. Üdvözöljük! - Önmegsemmisítési jelkód + Önmegsemmisítő-jelkód (beolvasás, vagy beillesztés a vágólapról) - Videóra várakozás + Várakozás a videóra Válasz - Ez az egyszer használatos hivatkozása! + Ez a saját egyszer használható meghívója! SimpleX Chat hívások - Új inkognító profil használata + Új inkognitóprofil használata Frissítse az alkalmazást, és lépjen kapcsolatba a fejlesztőkkel. SimpleX Hivatkozás előnézete - a biztonsági kód megváltozott - Kizárólag ismerős megjelenítése + a biztonsági kód módosult + Csak a partner nevének megjelenítése Hangszóró bekapcsolva Importált csevegési adatbázis használatához indítsa újra az alkalmazást. jogosulatlan küldés - Csak az ismerőse tud hangüzeneteket küldeni. + Csak a partnere tud hangüzeneteket küldeni. Beállítások - A kapcsolódáshoz az ismerőse beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást. + A kapcsolódáshoz a partnere beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást. visszaigazolás fogadása… - Biztonsági kód beolvasása az ismerősének alkalmazásából. - Lépjen kapcsolatba a csoport adminnal. + Biztonsági kód beolvasása a partnere alkalmazásából. + Lépjen kapcsolatba a csoport adminisztrátorával. Videó bekapcsolva - Profil neve: + Profilnév: Beillesztés Köszönjük, hogy telepítette a SimpleX Chatet! - Csillagozás a GitHub-on + Csillagozás a GitHubon Eltávolítás Keresés - Titkosítás újraegyeztetése? - Az önmegsemmisítési jelkód engedélyezve! + Újraegyezteti a titkosítást? + Az önmegsemmisítő-jelkód engedélyezve! Biztonsági kiértékelés Cím Üzenet elküldése Adatbázismentés visszaállítása - Visszavon - Ismerős felkérése, hogy engedélyezze a hangüzenetek küldését. - egyszer használatos hivatkozást osztott meg - 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. - ICE kiszolgálók - Kapcsolódás elfogadva + Visszavonás + Kérje meg a partnerét, hogy engedélyezze a hangüzenetek küldését. + Ön egy egyszer használható meghívót osztott meg + 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. + Saját ICE-kiszolgálók + Kapcsolat létrehozása Elutasítás - Ismerős és üzenet megjelenítése + Partner nevének és az üzenet tartalmának megjelenítése BEÁLLÍTÁSOK - Felhasználói fiók jelszavának mentése - Fájl küldés megállítása? - Számítógép leválasztása? - A hangüzenetek le vannak tilva! + Profiljelszó mentése + Megállítja a fájlküldést? + Leválasztja a számítógépet? + A hangüzenetek le vannak tiltva! Közvetlen üzenet küldése a kapcsolódáshoz - PING számláló + PING-ek száma Fejlesztői beállítások megjelenítése %s kapcsolódott Rendszer Amikor elérhető Hangüzenet (%1$s) %s (jelenlegi) - Saját kiszolgáló + Saját SMP-kiszolgáló Véletlen - Megosztás az ismerősökkel - ön - Nincsenek csevegési üzenetek + Megosztás a partnerekkel + Ön + Nincsenek csevegései Küldés %s másodperc %s: %s - A SimpleX nem tud futni a háttérben. Csak akkor fog értesítéseket kapni, ha az alkalmazás fut. + A SimpleX nem tud a háttérben futni. Csak akkor fog értesítéseket kapni, amikor az alkalmazás meg van nyitva. Túl sok kép! - Archívum mentése - %s, %s és %d tag + %s, %s és további %d tag Csevegési szolgáltatás megállítása - SimpleX hivatkozások - Az elküldött üzenetek törlésre kerülnek a beállított idő után. - Némítás feloldása - Elküldve ekkor: %s + SimpleX-hivatkozások + Az elküldött üzenetek törölve lesznek a beállított idő után. + Némítás megszüntetése + Elküldve: %s Jelenlegi profil használata Ez az eszköz - Megosztja az azonosítót az ismerőseivel? + Megosztja a címet a partnereivel? Profiljelszó Téma - Jelmondat eltávolítása a beállításokból? - SimpleX csoport hivatkozás - Képre várakozás + Eltávolítja a jelmondatot a beállításokból? + SimpleX-csoporthivatkozás + Várakozás a képre Önmegsemmisítés - várakozás válaszra… - Ismerős nevének beállítása… + várakozás a válaszra… + Partner nevének beállítása… Tag feloldása QR-kód beolvasása Kiszolgáló tesztelése @@ -1038,54 +1014,54 @@ Rendszer Elküldés Biztonsági kód - Adja meg a helyes aktuális jelmondatát. + Adja meg a helyes, jelenlegi jelmondatot. Az elküldött üzenetek végleges törlése le van tiltva. - Üzenetreakció tiltása. + A reakciók hozzáadása az üzenethez le van tiltva. Véletlenszerű jelmondat használata egyenrangú CSEVEGÉSI SZOLGÁLTATÁS INDÍTÁSA - Fogadott hivatkozás beillesztése - Kiszolgálók mentése? + Kapott hivatkozás beillesztése + Menti a kiszolgálókat? A SimpleX Chat biztonsága a Trail of Bits által lett auditálva. - módosított csoport profil - TÁMOGASSA A SIMPLEX CHATET + frissítette a csoport profilját + SIMPLEX CHAT TÁMOGATÁSA SimpleX Chat szolgáltatás Nem lehet üzeneteket küldeni! - %s ellenőrzött - Jelszó megjelenítése + %s hitelesítve + Jelszó a megjelenítéshez Adatvédelem és biztonság - Tag eltávolítása + Eltávolítás A jelkód beállítva! Elküldött üzenet - Ismerősök kiválasztása - ismeretlen üzenet formátum + Partnerek kijelölése + ismeretlen üzenetformátum Kiszolgálók mentése - Üdvözlő üzenet + Üdvözlőüzenet mp - A profilfrissítés elküldésre került az ismerősök számára. - Egyszerűsített inkognító mód - Üdvözlőszöveg mentése? + A profilfrissítés el lesz küldve a partnerei számára. + Egyszerűsített inkognitómód + Menti az üdvözlőüzenetet? Új csevegési fiók létrehozásához indítsa újra az alkalmazást. Engedély megtagadva! - Főggőben lévő hívás + Függőben lévő hívás Adatbázis megnyitása… - Leállítás? + Leállítja az alkalmazást? Jelmondat szükséges Privát értesítések - Meghívta egy ismerősét - %s nincs ellenőrizve - Koppintson a kapcsolódáshoz + Ön meghívta egy partnerét + %s nincs hitelesítve + Koppintson ide a kapcsolódáshoz Ennek az eszköznek a neve Jelenlegi profil Fájl feltöltése - Hang- és videóhívások tiltása. - Megkövetelt + A hívások kezdeményezése le van tiltva. + Szükséges SimpleX Chat üzenetek Visszaállítás - Adatbázis jelmondat beállítása - Elküldött üzenet + Adatbázis-jelmondat beállítása + Üzenetbuborék színe Időszakosan indul - Ez a SimpleX azonosítója! + Ez a saját SimpleX-címe! eltávolítva Megosztás SimpleX csapat @@ -1094,577 +1070,557 @@ tulajdonos Bekapcsolás %s, %s és %s kapcsolódott - SimpleX egyszer használatos meghívó + Egyszer használható SimpleX-meghívó Hívások nem sikerült elküldeni - TÉMA SZÍNEK - Visszaállít - Előző jelszó megadása az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza. - Másodlagos - SOCKS PROXY + KEZELŐFELÜLET SZÍNEI + 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. + Másodlagos szín + SOCKS-PROXY Mentés Újraindítás - Üzenetküldő (SMP) kiszolgálók + SMP-kiszolgálók Videó Automatikus elfogadási beállítások mentése - Újraegyzetetés - Videóra várakozás - XFTP kiszolgálók + Újraegyeztetés + Várakozás a videóra + Saját XFTP-kiszolgálók Videó kikapcsolva - Privát fájl nevek - Beállítások mentése? + Privát fájlnevek + Menti a beállításokat? Jelkód Ismeretlen hiba - Saját kiszolgáló cím - Csevegés konzol megnyitása - Tag eltávolítása - Adatbázis jelmondat beállítása + Saját SMP-kiszolgálójának címe + Csevegési konzol megnyitása + Eltávolítás + Adatbázis-jelmondat beállítása Biztonsági kód megtekintése - Tag feloldása? - A küldő törölhette a kapcsolódási kérelmet. - Téves adatbázis jelmondat - SMP kiszolgálók - Az üzenet kézbesítési jelentések le vannak tiltva - Adatbázis mappa megnyitása - egyszer használatos hivatkozáson keresztül + Feloldja a tag letiltását? + A küldője törölhette a meghívási kérést. + Érvénytelen adatbázis-jelmondat + Saját SMP-kiszolgálók + A kézbesítési jelentések le vannak tiltva + Adatbázismappa megnyitása + egy egyszer használható meghívón keresztül Csoportbeállítások megadása - ezen keresztül: %1$s + a következőn keresztül: %1$s igen Hangüzenet - Használat számítógépről - ÖN - port %d - Kapcsolódás hivatkozáson keresztül - Azonosító megosztása + Társítás számítógéppel + PROFIL + %d-s port + Kapcsolódás egy hivatkozáson keresztül + Cím megosztása A kiszolgáló QR-kódjának beolvasása Megállítás - Címmegosztás megállítása? - Csevegés profilok megnyitása - Csatlakozási kérés megismétlése? - Képre várakozás + Megállítja a címmegosztást? + Csevegési profilok módosítása + Megismétli a meghívási kérést? + Várakozás a képre Hangüzenetek - Tag eltávolítása? - Biztonsági kód ellenőrzése - eltávolítottak - SimpleX azonosító - Megjelenítés: - fogadott válasz… - Adatbázismentés visszaállítása? + Eltávolítja a tagot? + Biztonsági kód hitelesítése + eltávolította Önt + SimpleX-cím + Megjelenítve: + válasz fogadása… + Visszaállítja az adatbázismentést? Üzenetek fogadása… %s és %s kapcsolódott - megfigyelő szerep + Ön megfigyelő Port Jelkód beállítása - Változáslista + Újdonságok Csoport megnyitása - Elküldve ekkor: - Hangüzenetek küldése le van tiltva. - Utolsó üzenetek megjelenítése + Elküldve + A hangüzenetek küldése le van tiltva. + Legutóbbi üzenet előnézetének megjelenítése Az előre beállított kiszolgáló címe - Rendszeres értesítések letiltva! - A jelkód megváltozott! - Akkor fut, ha az alkalmazás nyitva van + Időszakos értesítések letiltva! + A jelkód módosult! + Akkor fut, amikor az alkalmazás meg van nyitva Ez a QR-kód nem egy hivatkozás! - Fájlra várakozás + Várakozás a fájlra simplexmq: v%s (%2s) Szétkapcsolás Véletlenszerű profil - Téves jelmondat! - Az üzenetreakciók küldése le van tiltva. + Érvénytelen jelmondat! + A reakciók hozzáadása az üzenetekhez le van tiltva. Rendszer olvasatlan - Függő + Függőben Üdvözöljük %1$s! - Jelmondat eltávolítása a Keystrore-ból? + Eltávolítja a jelmondatot a Keystrore-ból? Feloldás Az eltűnő üzenetek küldése le van tiltva. Videó Frissítés Megnyitás - Rendszeres értesítések + Időszakos értesítések Kihagyott üzenetek A hangüzenetek küldése le van tiltva. - Ismerős nevének beállítása - Csak ön tud eltűnő üzeneteket küldeni. - Kép/videó megoszása… - ön: %1$s + Partner nevének beállítása + Csak Ön tud eltűnő üzeneteket küldeni. + Médiatartalom megosztása… + Ön: %1$s Beállítások - Színek alaphelyzetbe állítása + Színek visszaállítása Mentés Váltás - Kapott hivatkozás beillesztése az ismerősökhöz történő kapcsolódáshoz… - Kód beolvasása - Port megnyitása a tűzfalon + A kapott hivatkozás beillesztése a partnerhez való kapcsolódáshoz… + Beolvasás + Port nyitása a tűzfalban indítás… - Szín mentése Leállítás elküldve - SOCKS proxy használata + SOCKS-proxy használata Élő üzenet küldése - Adatvédelem újraértelmezve + Újraértelmezett adatvédelem Hangüzenet… Alkalmazás képernyőjének védelme QR-kód megjelenítése videóhívás - Nem kedvenc - Üzenet kézbesítési jelentések küldése - SimpleX azonosító + Kedvenc megszüntetése + Kézbesítési jelentések küldése + SimpleX-cím Koppintson a - Mentés és ismerős értesítése + Mentés és a partner értesítése Elutasított hívás - SOCKS proxy beállítások + SOCKS-proxybeállítások QR-kód Titkosítás újraegyeztetése Eltávolítás - Tor .onion kiszolgálók használata + Onion-kiszolgálók használata Felfedés - SimpleX zárolási mód + Zárolási mód Fájl visszavonása - XFTP kiszolgálók - A fájlok- és a médiatartalom küldése le van tiltva. + XFTP-kiszolgálók + A fájlok- és a médiatartalmak küldése le van tiltva. Fájl megosztása… Mentés - átjátszón keresztül + egy továbbítókiszolgálón keresztül Megosztás megállítása - eltávolította őt: %1$s - Jelmondat mentése és csevegés megnyitása - Beállítások mentése? - Az első csevegési rendszer bármiféle felhasználó azonosító nélkül - privátra lett tervezre. - A közvetlen üzenetek küldése a tagok számára le van tiltva. - SOCKS proxy használata? + Ön eltávolította őt: %1$s + Jelmondat mentése és a csevegés megnyitása + Menti a beállításokat? + Nincsenek felhasználó-azonosítók. + A közvetlen üzenetek küldése a tagok között le van tiltva. + SOCKS-proxy használata? Hangszóró kikapcsolva hét Megjelenítés - WebRTC ICE kiszolgálók - Fájl visszavonása? + WebRTC ICE-kiszolgálók + Visszavonja a fájlt? Közvetlen üzenet küldése Elutasítás Küldés - Rendszerhitelesítés + Rendszer-hitelesítés Böngészőn keresztül - Csevegési profiljok védelme jelszóval! - Csak az ismerőse tud eltűnő üzeneteket küldeni. - ICE kiszolgálók - QR-kód beolvasása számítógépről + Védje meg a csevegési profiljait egy jelszóval! + Csak a partnere tud eltűnő üzeneteket küldeni. + Saját ICE-kiszolgálók + QR-kód beolvasása a számítógépről SimpleX logó Feloldás - Némítás feloldása + Némítás megszüntetése SimpleX Chat megnyitása a hívás fogadásához - Fájl fogadás megállítása? - - opcionális értesítés a törölt ismerősök számára -\n- profil nevek szóközökkel -\n- és továbbiak! + Megállítja a fájlfogadást? + - 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! Lengyel kezelőfelület Kiszolgáló használata - Fogadva ekkor: %s - SimpleX zárolás - Mentés és csoporttagok értesítése - Alaphelyzetbe állítás - Csak az ismerőse tud üzenetreakciókat küldeni. + Fogadva: %s + SimpleX-zár + Mentés és a csoporttagok értesítése + Visszaállítás + Csak a partnere adhat hozzá reakciókat az üzenetekhez. Hangüzenetek - elhagyta a csoportot + Ön elhagyta a csoportot Hangüzenet rögzítése - SimpleX zárolás bekapcsolva + SimpleX-zár bekapcsolva közvetlen üzenet küldése - Beolvasás mobilról - Kapcsolatok ellenőrzése + Beolvasás hordozható eszközről + Kapcsolatok hitelesítése Üzenet megosztása… másodperc - SimpleX zárolás nincs engedélyezve! - SimpleX zárolás + A SimpleX-zár nincs bekapcsolva! + SimpleX-zár Beállítások Csevegési adatbázis - %1$s eltávolítva - Sikertelen kiszolgáló-teszt! - Kapcsolat ellenőrzése + eltávolította őt: %1$s + Sikertelen kiszolgáló teszt! + Kapcsolat hitelesítése Tudjon meg többet - A küldő megszakította a fájl átvitelt. - Csevegési szolgáltatás megállítása? - Fogadva ekkor: + A fájl küldője visszavonta az átvitelt. + Megállítja a csevegést? + Fogadva Beállítva 1 nap Felfedés - Fogadott üzenet - Csak az ismerőse tudja az üzeneteket véglegesen törölni (ön csak törlésre jelölheti meg azokat). (24 óra) - Az önmegsemmisítési jelkód megváltozott! - SimpleX Chat kiszolgálók használatban. - SimpleX Chat kiszolgálók használata? + Fogadott üzenetbuborék színe + Csak a partnere tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra) + Az önmegsemmisítő-jelkód módosult! + SimpleX Chat-kiszolgálók használatban. + SimpleX Chat-kiszolgálók használata? Csevegési profil felfedése - Videók és fájlok 1Gb méretig - TCP kapcsolat időtúllépés - A(z) %1$s nevű profiljának SimpleX azonosítója megosztásra fog kerülni. - Ön már kapcsolódott ehhez: %1$s. - Jelenlegi csevegési adatbázis TÖRLÉSRE és FELCSERÉLÉSRE kerül az importált által! -\nEz a művelet nem vonható vissza - profiljai, ismerősei, csevegési üzenetei és fájljai véglegesen törölve lesznek! + Videók és fájlok legfeljebb 1GB méretig + TCP-kapcsolat időtúllépése + A(z) %1$s nevű profilja meg lesz osztva. + Ön már kapcsolódott a következőhöz: %1$s. + 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. Ötletek és javaslatok Figyelmeztetés: néhány adat elveszhet! - Koppintson az új csevegés indításához + Koppintson ide az új csevegés indításához Várakozás a számítógépre… - A privát üzenetküldés következő generációja - Hálózati beállítások megváltoztatása? - Várakozás a mobiltelefon társítására: - Kapcsolat biztonságának ellenőrzése + Az üzenetváltás jövője + Módosítja a hálózati beállításokat? + Várakozás a hordozható eszköz társítására: + Biztonságos kapcsolat hitelesítése fájlok küldése egyelőre még nem támogatott - azonosítója erre változott: %s + Ön módosította a címet %s számára fájlok fogadása egyelőre még nem támogatott - Csoport profil mentése - Alaphelyzetbe állítás - Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát. -\nA kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. + Csoportprofil mentése + Visszaállítás alapértelmezettre + 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. videóhívás (nem e2e titkosított) Alkalmazás új kapcsolatokhoz - Az új üzenetek rendszeresen letöltésre kerülnek az alkalmazás által – 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 kerülnek elküldésre a kiszolgálóknak. - Számítógép azonosítójának beillesztése - kapcsolattartási azonosító-hivatkozáson keresztül - SimpleX háttérszolgáltatást használja - az akkumulátor néhány százalékát használja naponta.]]> - Az ismerősnek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön. -\nMegszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással) - A jelszó 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. - Az ismerősei továbbra is kapcsolódva maradnak. - A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát - Az adatbázis nem működik megfelelően. Koppintson további információért - A fájl küldése leállt. - Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. - Nem lehetett ellenőrizni; próbálja meg újra. - Az üzenet minden tag számára moderáltként lesz megjelölve. + 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. + Számítógép címének beillesztése + a kapcsolattartási címhivatkozáson keresztül + a SimpleX a háttérben fut a push értesítések használata helyett.]]> + 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). + 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. + A partnerei továbbra is kapcsolódva maradnak. + A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát + Az adatbázis nem működik megfelelően. Koppintson ide a további információkért + A fájl küldése le fog állni. + Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál. + Nem sikerült hitelesíteni; próbálja meg újra. + Az üzenet az összes tag számára moderáltként lesz megjelölve. Értesítések fogadásához adja meg az adatbázis jelmondatát A teszt a(z) %s lépésnél sikertelen volt. - Az alkalmazás indításakor, vagy 30 másodpercnyi háttérben töltött idő után az alkalmazáshoz visszatérve hitelesítés szükséges. - Az üzenet minden tag számára törlésre kerül. + 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. + Az üzenet az összes tag számára törölve lesz. A videó nem dekódolható. Próbálja ki egy másik videóval, vagy lépjen kapcsolatba a fejlesztőkkel. - Ez a szöveg a beállítások között érhető el - Profilja elküldésre kerül ismerőse számára, akitől ezt a hivatkozást kapta. + Ez a szöveg a „Beállításokban” érhető el + A profilja el lesz küldve a partnere számára, akitől ezt a hivatkozást kapta. Az alkalmazás 1 perc után bezárható a háttérben. - meghívást kapott a csoportba - engedélyezze a SimpleX háttérben történő futását a következő párbeszédpanelen. Ellenkező esetben az értesítések letiltásra kerülnek.]]> - A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát + Ön meghívást kapott a csoportba + Engedélyezze a következő párbeszédpanelen az azonnali értesítések fogadásához.]]> + A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát Kapcsolódni fog a csoport összes tagjához. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen - Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót. -\nA funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére. + 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. A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! - Hálózati kapcsolat ellenőrzése a következővel: %1$s, és próbálja újra. - A SimpleX zárolás a Beállításokon keresztül kapcsolható be. + Ellenőrizze a hálózati kapcsolatát vele: %1$s, és próbálja újra. + A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be. Az alkalmazás összeomlott - Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg ismerősét, hogy küldjön egy másikat. + Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg a partnerét, hogy küldjön egy másikat. A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel. - Érvénytelen fájl elérési útvonalat osztott meg. Jelentse a problémát az alkalmazás fejlesztőinek. - Már van egy csevegési profil ugyanezzel a megjelenített névvel. Válasszon egy másik nevet. - Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %1$s). - A fájl fogadása leállt. + Érvénytelen fájlelérési útvonalat osztott meg. Jelentse a problémát az alkalmazás fejlesztőinek. + Már van egy csevegési profil ugyanezzel a megjelenítendő névvel. Válasszon egy másik nevet. + 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). + A fájl fogadása le fog állni. Ne felejtse el, vagy tárolja biztonságosan – az elveszett jelszót nem lehet visszaállítani! A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését. - egyszer használatos hivatkozást osztott meg inkognitóban - Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. - Később engedélyezheti a Beállításokban + Ön egy egyszer használható meghívót osztott meg inkognitóban + Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál. + Később engedélyezheti a „Beállításokban” Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! különböző átköltöztetés az alkalmazásban/adatbázisban: %s / %s - %1$s.]]> + %1$s.]]> Profil felfedése - Ez a hivatkozás nem érvényes kapcsolati hivatkozás! - A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy szkennelje be) az ismerőse eszközén lévő kódot. - 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 ismerősétől. - Ez a beállítás a jelenlegi csevegési profilban lévő üzenetekre érvényes - Meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. + Ez nem egy érvényes kapcsolattartási hivatkozás! + 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. + 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. + Ez a beállítás csak az Ön jelenlegi csevegési profiljában lévő üzenetekre vonatkozik + Ön meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. Ez a csoport már nem létezik. A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül. - Meghívást kapott a csoportba - Ismerőse a jelenleg megengedett maximális méretű (%1$s) fájlnál nagyobbat küldött. - Az ismerősei és az üzenetek (kézbesítés után) nem kerülnek tárolásra a SimpleX kiszolgálókon. + Ön meghívást kapott a csoportba + A partnere a jelenleg megengedett maximális méretű (%1$s) fájlnál nagyobbat küldött. + A partnerei és az üzenetek (kézbesítés után) nem a SimpleX-kiszolgálókon vannak tárolva. Üzenetek formázása a szövegbe szúrt speciális karakterekkel: - Megnyitás alkalmazásban gombra.]]> - Csevegési profilja elküldésre kerül -\naz ismerőse számára - Egy olyan ismerősét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a saját fő profilja van használatban - %1$s csoporthoz.]]> + Megnyitás az alkalmazásban gombra.]]> + A csevegési profilja el lesz küldve\na partnere számára + Egy olyan partnerét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a fő profilja van használatban + %1$s nevű csoporthoz.]]> Amikor az alkalmazás fut - Inkognító profilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében meghívók küldése tiltott - Kapcsolat izolációs mód - Akkor lesz kapcsolódva, ha a kapcsolódási kérelme elfogadásra kerül, várjon, vagy ellenőrizze később! - A hangüzenetek küldése le van tiltva ebben a csoportban. - Alkalmazás akkumulátor használata / Korlátlan módot az alkalmazás beállításaiban.]]> - Biztonságos kvantumrezisztens protokollon keresztül. - - hangüzenetek 5 percig. -\n- egyedi eltűnési időhatár. -\n- előzmény szerkesztése. - Használat számítógépről menüpontot a mobil alkalmazásban és olvassa be a QR-kódot.]]> + 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 + Átvitel-izoláció + Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később! + A hangüzenetek küldése le van tiltva. + Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> + Biztonságos kvantumbiztos protokollon keresztül. + - legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése. + Társítás számítógéppel menüt a hordozható eszköz alkalmazásban és olvassa be a QR-kódot.]]> %s ekkor: %s - Akkor lesz kapcsolódva, amikor az ismerősének az eszköze online lesz, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva, amikor a partnerének az eszköze online lesz, várjon, vagy ellenőrizze később! Kéretlen üzenetek elrejtése. - Használja az .onion kiszolgálókat NEM értékre, ha a SOCKS proxy nem támogatja őket.]]> - Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz. + Onion-kiszolgálók használata opciót „Nemre”, ha a SOCKS-proxy nem támogatja őket.]]> + Megoszthatja a címét egy hivatkozásként vagy egy QR-kódként – így bárki kapcsolódhat Önhöz. Létrehozás később - Profilja az eszközön van tárolva és csak az ismerőseivel kerül megosztásra. A SimpleX kiszolgálók nem láthatják a profilját. - %s szerepkörét megváltoztatta erre: %s - Csoport meghívó elutasítva - Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználói azonosítók helyett, a SimpleX üzenetsorokhoz rendel azonosítókat, minden egyes ismerőshöz egy különbözőt. - (megosztás egy ismerőssel) - Csoport meghívó elküldve - Kapcsolat izolációs mód frissítése? - Kapcsolat izolációs mód + 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. + Ön a következőre módosította %s szerepkörét: „%s” + Csoportmeghívó elutasítva + Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. + (a megosztáshoz a partnerével) + Csoportmeghívó elküldve + Frissíti az átvitel-izoláció módját? + Átvitel-izoláció Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak. - A csevegési adatbázis nem titkosított - állítson be egy jelmondatot annak védelméhez. - Közvetlen internet kapcsolat használata? + A csevegési adatbázis nem titkosított – állítson be egy jelmondatot annak védelméhez. + Közvetlen internetkapcsolat használata? Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. - A fő csevegési profilja megküldésre kerül a csoporttagok számára - Később engedélyezheti őket az alkalmazás Adatvédelem és biztonság menüpontban. - Rejtett profiljának felfedéséhez írja be a teljes jelszót a Csevegési profilok oldal keresőmezőjébe. - A csevegés frissítése és megnyitása - Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősei számára. - fogadja az üzeneteket, ismerősöket – a kiszolgálók, amelyeket az üzenetküldéshez használ.]]> - %1$s csoport tagja.]]> - azonosítója megváltoztatva - Ismerősei engedélyezhetik a teljes üzenet törlést. - A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul - nem az eszközön kerül tárolásra. - Ha engedélyezni szeretné, hogy egy mobilalkalmazás csatlakozzon a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, ha engedélyezte azt - Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra. - Alkalmazás akkumulátor használata / Korlátlan módot az alkalmazás beállításaiban.]]> - Ez a karakterlánc nem egy meghívó hivatkozás! - Új csevegés kezdése - A kapcsolódás már folyamatban van ezen az egyszer használatos hivatkozáson keresztül! - Nem veszíti el az ismerőseit, ha később törli az azonosítóját. + A fő csevegési profilja el lesz küldve a csoporttagok számára + Később engedélyezheti őket az „Adatvédelem és biztonság” menüben. + Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a „Csevegési profilok” menüben. + Fejlesztés és a csevegés megnyitása + Engedélyeznie kell a hangüzenetek küldését a partnere számára, hogy hangüzeneteket küldhessenek egymásnak. + %1$s nevű csoport tagja.]]> + Ön módosította a címet + A partnerei engedélyezhetik a teljes üzenet törlését. + A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul – nem az eszközön van tárolva. + 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 + A profilja, a partnerei és az elküldött üzenetei a saját eszközén vannak tárolva. + Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> + Ez a karakterlánc nem egy meghívási hivatkozás! + Új csevegés indításához + A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül! + Nem veszíti el a partnereit, ha később törli a címét. A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár. - kapcsolatba akar lépni veled! - saját szerepköre erre változott: %s - A csevegési szolgáltatás elindítható a Beállítások / Adatbázis menüpontban vagy az alkalmazás újraindításával. - Kód ellenőrzése a mobilon - Csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. - a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet az újdonságokról.]]> - Opcionális üdvözlő üzenettel. - Ismeretlen adatbázis hiba: %s - Elrejthet vagy némíthat egy felhasználói profilt - tartsa lenyomva a menühöz. - Inkognító mód kapcsolódáskor. - Tor .onion kiszolgálók beállításainak frissítése? - Megoszthat egy hivatkozást vagy QR-kódot - így bárki csatlakozhat a csoporthoz. Ha a csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait. - Csatlakozott ehhez a csoporthoz - %1$s csoporthoz!]]> - A hangüzenetek le vannak tiltva ebben a csevegésben. + kapcsolatba akar lépni Önnel! + Ön a következőre módosította a saját szerepkörét: „%s” + 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. + Kód hitelesítése a hordozható eszközön + Ön csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. + a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet a friss hírekről.]]> + Nem kötelező üdvözlőüzenettel. + Ismeretlen adatbázishiba: %s + 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. + Inkognitóra váltás kapcsolódáskor. + 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. + Ön csatlakozott ehhez a csoporthoz + %1$s nevű csoporthoz!]]> + A hangüzenetek küldése le van tiltva ebben a csevegésben. Ön irányítja csevegését! - Kód ellenőrzése a számítógépen - Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak. - Csoporttag részére a kapcsolódási kérelem elküldésre kerül. + Kód hitelesítése a számítógépen + Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak. + A meghívási kérés el lesz küldve ezen csoporttag számára. Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott. - Már kért egy kapcsolódási kérelmet ezen az azonosítón keresztül! - Megoszthatja ezt a SimpleX azonosítót az ismerőseivel, hogy kapcsolatba léphessenek vele: %s. - Amikor az emberek kapcsolódást kérelmeznek, ön elfogadhatja vagy elutasíthatja azokat. + Már küldött egy meghívási kérést ezen a címen keresztül! + Megoszthatja ezt a SimpleX-címet a partnereivel, hogy kapcsolatba léphessenek vele: %s. + Amikor az emberek meghívót küldenek, Ön elfogadhatja vagy elutasíthatja azokat. Megjelenítendő üzenet beállítása az új tagok számára! - Köszönet a felhasználóknak - hozzájárulás a Weblaten! - A kézbesítési jelentés küldése minden ismerős számára engedélyezésre kerül. - Protokoll időkorlát KB-onként - Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be. - Ez a művelet nem vonható vissza - a kiválasztottnál korábban küldött és fogadott üzenetek törlésre kerülnek. Ez több percet is igénybe vehet. - Profilja csak az ismerőseivel kerül megosztásra. + Köszönet a felhasználóknak a Weblate-en való közreműködésért! + A kézbesítési jelentések küldése az összes partnere számára engedélyezve lesz. + Protokoll időtúllépése kB-onként + Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be. + 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. + A profilja csak a partnereivel van megosztva. Néhány kiszolgáló megbukott a teszten: - Koppintson a csatlakozáshoz - Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású fotók viszont megmaradnak. - Kézbesítési jelentések engedélyezve vannak %d ismerősnél - Küldés ezen keresztül: - Köszönet a felhasználóknak - hozzájárulás a Weblaten! - A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő minden ismerős számára. + Koppintson ide a csatlakozáshoz + 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. + A kézbesítési jelentések engedélyezve vannak %d partnernél + Küldés a következőn keresztül: + Köszönet a felhasználóknak a Weblate-en való közreműködésért! + 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. Bluetooth támogatás és további fejlesztések. Ez a funkció még nem támogatott. Próbálja meg a következő kiadásban. - A bejegyzés frissítve: %s + Bejegyzés frissítve: %s Tagok meghívásának kihagyása - Ezek felülbírálhatóak az ismerős- és csoportbeállításokban. - Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! - A véletlenszerű jelmondat egyszerű szövegként van tárolva a beállításokban. -\nEz később megváltoztatható. - Koppintson az inkognitóban való kapcsolódáshoz + Ezek felülbírálhatók a partner- és csoportbeállításokban. + A partnere, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! + A véletlenszerű jelmondat egyszerű szövegként van tárolva a beállításokban.\nEzt később módosíthatja. + Koppintson ide az inkognitóban való kapcsolódáshoz Jelmondat beállítása az exportáláshoz - Kézbesítési jelentések le vannak tiltva a(z) %d csoportban - Néhány nem végzetes hiba történt az importálás során – további részletekért a csevegési konzolban olvashat. - Köszönet a felhasználóknak - hozzájárulás a Weblaten! - Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címét. - Rendszerhitelesítés helyetti beállítás. - A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be. - A csevegés megállítása a csevegő adatbázis exportálásához, importálásához, vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni. - Jelmondat mentése a kulcstárolóba - Köszönet a felhasználóknak - hozzájárulás a Weblaten! + A kézbesítési jelentések le vannak tiltva %d csoportban + Néhány nem végzetes hiba történt az importáláskor: + Köszönet a felhasználóknak a Weblate-en való közreműködésért! + A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet. + Beállítás a rendszer-hitelesítés helyett. + 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. + A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. + Jelmondat mentése a Keystore-ba + Köszönet a felhasználóknak a Weblate-en való közreműködésért! Jelmondat mentése a beállításokban - Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem kerülnek elküldésre. + Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem lesznek elküldve. A második jelölés, amit kihagytunk! ✅ - Az átjátszó kiszolgáló megvédi IP-címét, de megfigyelheti a hívás időtartamát. - További információ a GitHub tárolónkban. + A továbbítókiszolgáló megvédi az Ön IP-címét, de megfigyelheti a hívás időtartamát. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. - A mentett WebRTC ICE-kiszolgálók eltávolításra kerülnek. - Kézbesítési jelentések engedélyezve vannak a(z) %d csoportban - A szerepkör meg fog változni erre: \"%s\". A csoportban mindenki értesítve lesz. + A mentett WebRTC ICE-kiszolgálók el lesznek távolítva. + A kézbesítési jelentések engedélyezve vannak %d csoportban + A tag szerepköre a következőre fog módosulni: „%s”. A csoport összes tagja értesítést fog kapni. Profil és kiszolgálókapcsolatok - Üzenetküldő és alkalmazásplatform, amely védi az ön adatvédelmét és biztonságát. + Egy üzenetváltó- és alkalmazásplatform, amely védi az adatait és biztonságát. A profil aktiválásához koppintson az ikonra. - Kézbesítési jelentések le vannak tiltva %d ismerősnél + A kézbesítési jelentések le vannak tiltva %d partnernél Munkamenet kód - Köszönet a felhasználóknak - hozzájárulás a Weblaten! + Köszönet a felhasználóknak a Weblate-en való közreműködésért! Kis csoportok (max. 20 tag) - Az ön által elfogadott kapcsolat megszakad! - Élő üzenet küldése - a címzett(ek) számára frissül, ahogy beírja + Az Ön által elfogadott kérelem vissza lesz vonva! + É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 A KÉZBESÍTÉSI JELENTÉSEKET A KÖVETKEZŐ CÍMRE KELL KÜLDENI - A következő üzenet azonosítója hibás (kisebb vagy egyenlő az előzővel). -\nEz valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. - Az eszköz neve megosztásra kerül a csatlakoztatott mobil klienssel. - A címzettek a beírás közben látják a frissítéseket. - Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja megváltoztatni. - A jelmondat a beállítások között egyszerű szövegként kerül tárolásra, miután megváltoztatta vagy újraindította az alkalmazást. - A jelenlegi csevegési profilhoz tartozó új kapcsolatok kiszolgálói - Fogadás ezen keresztül: + 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ő. + Az eszköz neve meg lesz osztva a társított hordozható eszközön használt alkalmazással. + A címzettek a beírás közben látják a szövegváltozásokat. + Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja módosítani. + 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. + A jelenlegi csevegési profiljához tartozó új kapcsolatok kiszolgálói + Fogadás a következőn keresztül: Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez. - A tag szerepköre erre fog változni: \"%s\". A tag új meghívót fog kapni. - profilkép helyőrző + A tag szerepköre a következőre fog módosulni: „%s”. A tag új meghívást fog kapni. + profilkép helyőrzője A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet! - Ez a művelet nem vonható vissza - profiljai, ismerősei, üzenetei és fájljai véglegesen törlésre kerülnek. - A bejegyzés frissítve + Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek. + Bejegyzés frissítve Használati útmutatóban olvasható.]]> A jelmondat a beállításokban egyszerű szövegként van tárolva. Konzol megjelenítése új ablakban - Az előző üzenet hash-e más. - Ezek a beállítások a jelenlegi profiljára vonatkoznak - Várjon, amíg a fájl betöltődik a csatolt mobilról + Az előző üzenet hasítóértéke különbözik. + Ezek a beállítások csak a jelenlegi csevegési profiljára vonatkoznak + Várjon, amíg a fájl betöltődik a társított hordozható eszközről GitHub tárolónkban.]]> - hiba a tartalom megjelenítése közben - hiba az üzenet megjelenítésekor - Láthatóvá teheti SimpleX-beli ismerősei számára a Beállításokban. - Legfeljebb az utolsó 100 üzenet kerül elküldésre az új tagok számára. - A beolvasott kód nem egy SimpleX hivatkozás QR-kód. - A beillesztett szöveg nem egy SimpleX hivatkozás. - A meghívó hivatkozását újra megtekintheti a kapcsolat részleteinél. - Csevegés indítása? + Hiba történt a tartalom megjelenítésekor + Hiba történt az üzenet megjelenítésekor + Láthatóvá teheti a SimpleXbeli partnerei számára a „Beállításokban”. + Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára. + A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás. + A beillesztett szöveg nem egy SimpleX-hivatkozás. + A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. + Elindítja a csevegést? Látható előzmények Alkalmazás jelkód - Ismerős hozzáadása - Koppintson a beolvasáshoz - Koppintson a hivatkozás beillesztéséhez - Ismerős hozzáadása: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz.]]> + Partner hozzáadása + Koppintson ide a QR-kód beolvasásához + Koppintson ide a hivatkozás beillesztéséhez + 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.]]> Csoport létrehozása: új csoport létrehozásához.]]> - A csevegés leá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 megkezdése előtt. - Az előzmények nem kerülnek elküldésre az új tagok számára. + 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. + Az előzmények nem lesznek elküldve az új tagok számára. Újrapróbálkozás - A fényképező nem elérhető - Az utolsó 100 üzenet elküldése az új tagok számára. - Az előzmények ne kerüljenek elküldésre az új tagok számára. + A kamera nem elérhető + Legfeljebb az utolsó 100 üzenet elküldése az új tagok számára. + Az előzmények ne legyenek elküldve az új tagok számára. Vagy mutassa meg ezt a kódot Kamera hozzáférés engedélyezése - Fel nem használt meghívó megtartása? - Egyszer használatos meghívó hivatkozás megosztása - Új beszélgetés + Megtartja a fel nem használt meghívót? + Ennek az egyszer használható meghívónak a megosztása + Új csevegés Csevegések betöltése… Hivatkozás létrehozása… Vagy QR-kód beolvasása Érvénytelen QR-kód Megtartás - Keresés, vagy SimpleX hivatkozás beillesztése + Keresés vagy SimpleX-hivatkozás beillesztése Belső hibák megjelenítése Kritikus hiba Belső hiba - Nem támogatott számítógép kliens verzió. Győződjön meg arról, hogy mindkét eszközön ugyanazt a verziót használja - Számítógép kliensen hibás meghívókód szerepel + 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 + A számítógépes alkalmazásban érvénytelen meghívókód szerepel Számítógép elfoglalt Számítógép inaktív Csevegés újraindítása - Időtúllépés a számítógépről való kapcsolódás közben - Számítógép kliens kapcsolata megszakadt + Időtúllépés a számítógéphez való csatlakozáskor + Kapcsolat bontva a számítógéppel A kapcsolat megszakadt A kapcsolat megszakadt - Számítógép kapcsolata rossz állapotban van - Jelentse a fejlesztőknek: -\n%s -\n -\nAz alkalmazás újraindítása javasolt. + A kapcsolat a számítógéppel rossz állapotban van + Jelentse a fejlesztőknek:\n%s\n\nAz alkalmazás újraindítása javasolt. Jelentse a fejlesztőknek: \n%s - %s mobil eszköz verziója nem támogatott. Győződjön meg arról, hogy mindkét eszközön ugyanazt a verziót használja]]> - %s mobil eszközzel]]> - Érvénytelen megjelenítendő felhaszálónév! - Ez a megjelenített felhasználónév érvénytelen. Válasszon egy másik nevet. - %s mobil eszközzel, a(z) %s probléma miatt]]> - %s probléma miatt megszakadt a kapcsolat - %s mobil eszköz nem található]]> - %s mobil eszközzel rossz állapotban van]]> - %s mobil eszközhöz való csatlakozás közben]]> + %s 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]]> + %s nevű hordozható eszközzel]]> + Érvénytelen megjelenítendő név! + Ez a megjelenítendő név érvénytelen. Válasszon egy másik nevet. + %s nevű hordozható eszközzel, a következő okból: %s]]> + Kapcsolat bontva a következő okból: %s + %s hordozható eszköz nem található]]> + %s hordozható eszközzel rossz állapotban van]]> + %s hordozható eszközhöz való csatlakozáskor]]> ismeretlen Lassú funkció Lassú API-hívások megjelenítése - %s mobil eszköz inaktív]]> + %s hordozható eszköz inaktív]]> Fejlesztői beállítások A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s - %s mobil eszköz elfoglalt]]> - Legutóbbi tag %1$s - ismeretlen státusz - %1$s megváltoztatta a nevét erre: %2$s - törölt kapcsolattartási azonosító - törölt profilkép - új kapcsolattartási azonosító beállítása - új profilkép beállítása + %s hordozható eszköz elfoglalt]]> + (Már nem tag) %1$s + ismeretlen állapot + %1$s a következőre módosította a nevét: %2$s + eltávolította a kapcsolattartási címet + eltávolította a profilképét + új kapcsolattartási címet állított be + új profilképet állított be frissített profil - %1$s megváltoztatta a nevét erre: %2$s + %1$s a következőre módosította a nevét: %2$s Privát jegyzetek - Hiba a privát jegyzetek törlésekor - Hiba az üzenet létrehozásakor - Privát jegyzetek törlése? - Létrehozva ekkor: + Hiba történt a privát jegyzetek törlésekor + Hiba történt az üzenet létrehozásakor + Kiüríti a privát jegyzeteket? + Létrehozva Mentett üzenet - Megosztva ekkor: %s - Minden üzenet törlésre kerül – ez a művelet nem vonható vissza! - Továbbfejlesztett üzenetküldés + Létrehozva: %s + Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! + Továbbfejlesztett üzenetkézbesítés Csatlakozás csoportos beszélgetésekhez Hivatkozás beillesztése a kapcsolódáshoz! Privát jegyzetek - A keresősáv elfogadja a meghívó hivatkozásokat. - Titkosított fájlokkal és médiatartalommal. - Csökkentett akkumulátorhasználattal. - Magyar és török felhasználói felület - A közelmúlt eseményei és továbbfejlesztett jegyzék bot. - %s letiltása feloldva - %s letiltását visszavonta + A keresősáv elfogadja a meghívási hivatkozásokat. + Titkosított fájlokkal és médiatartalmakkal. + Csökkentett akkumulátor-használattal. + Magyar és török kezelőfelület + A közelmúlt eseményei és továbbfejlesztett könyvtárbot. + feloldotta %s letiltását + Ön feloldotta %s letiltását letiltva - letiltva az admin által - Letiltva az admin által - %s letiltva - Mindenki számára letiltva - Tag letiltása mindenki számára? - %d üzenet letiltva az admin által - Letiltás feloldása mindenki számára - Mindenki számára feloldja a tag letiltását? - letiltotta %s-t - Hiba a tag mindenki számára való letiltása során + letiltva az adminisztrátor által + Letiltva az adminisztrátor által + letiltotta őt: %s + Letiltás + Az összes tag számára letiltja a tagot? + %d üzenetet letiltott az adminisztrátor + Feloldás + Az összes tag számára feloldja a tag letiltását? + Ön letiltotta őt: %s + Hiba történt a tag az összes csoporttag számára való letiltásakor Az üzenet túl nagy - Az üdvözlő üzenet túl hosszú - Az adatbázis átköltöztetése folyamatban van. -\nEz eltarthat néhány percig. + Az üdvözlőüzenet túl hosszú + Az adatbázis átköltöztetése folyamatban van.\nEz eltarthat néhány percig. Hanghívás A hívás befejeződött Videóhívás - Hiba a böngésző megnyitása közben + Hiba történt a böngésző megnyitásakor 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. Hálózati beállítások megerősítése - Hiba a csevegési adatbázis exportálásakor + Hiba történt a csevegési adatbázis exportálásakor Alkalmaz Archiválás és feltöltés Feltöltés megerősítése - Hiba az adatbázis törlésekor - Az adminok egy tagot mindenki számára letilthatnak. - Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP átjátszókra. + Hiba történt az adatbázis törlésekor + Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak. + Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra. Alkalmazásadatok átköltöztetése Adatbázis archiválása Átköltöztetés visszavonása A csevegés átköltöztetve! Ellenőrizze az internetkapcsolatot, és próbálja újra - Archív hivatkozás létrehozása + Archívum hivatkozás létrehozása Adatbázis törlése erről az eszközről Sikertelen letöltés Archívum letöltése Letöltési hivatkozás részletei Engedélyezés a közvetlen csevegésekben (BÉTA)! - Jelmondat megadása - Hiba a beállítások mentésekor - Hiba az archívum letöltésekor - Hiba az archívum feltöltésekor - Hiba a jelmondat ellenőrzésekor: + Adja meg a jelmondatot + Hiba történt a beállítások mentésekor + Hiba történt az archívum letöltésekor + Hiba történt az archívum feltöltésekor + Hiba történt a jelmondat hitelesítésekor: Az exportált fájl nem létezik - A fájl törlésre került, vagy érvénytelen hivatkozás + A fájl törölve lett, vagy érvénytelen a hivatkozás %s letöltve Archívum importálása Feltöltés előkészítése - Adatbázis jelmondatának ellenőrzése - Jelmondat ellenőrzése + Az adatbázis jelmondatának hitelesítése + Jelmondat hitelesítése Jelmondat beállítása Kép a képben hívások Biztonságosabb csoportok @@ -1672,15 +1628,15 @@ Vagy az archívum hivatkozásának beillesztése Archívum hivatkozásának beillesztése Letöltés ismét - Sikertelen importálás - Ellenőrizze, hogy a hálózati beállítások megfelelőek-e ehhez az eszközhöz. + Nem sikerült az importálás + Ellenőrizze, hogy a hálózati beállítások megfelelők-e ehhez az eszközhöz. A folytatáshoz a csevegést meg kell szakítani. Csevegés megállítása folyamatban - Vagy a fájl hivítkozásának biztonságos megosztása + Vagy ossza meg biztonságosan ezt a fájlhivatkozást Csevegés indítása Nem szabad ugyanazt az adatbázist használni egyszerre két eszközön.]]> - Erősítse meg, hogy emlékszik az adatbázis jelmondatára az átköltöztetéshez. - Átköltöztetés egy másik eszközről opciót az új eszközön és szkennelje be a QR-kódot.]]> + Az átköltöztetéshez erősítse meg, hogy emlékszik az adatbázis jelmondatára. + Átköltöztetés egy másik eszközről opciót az új eszközén és olvassa be a QR-kódot.]]> Átköltöztetés véglegesítése Átköltöztetés véglegesítése egy másik eszközön. Letöltés előkészítése @@ -1688,29 +1644,29 @@ %s feltöltve Sikertelen feltöltés Archívum feltöltése - Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, továbbá üzenetkézbesítési hibákat okozhat + 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 Importálás ismét szabványos végpontok közötti titkosítás Átköltöztetés ide Eszköz átköltöztetése Átköltöztetés egy másik eszközre - Figyelem: az archívum törlésre kerül.]]> + Figyelmeztetés: az archívum törölve lesz.]]> Átköltöztetés egy másik eszközről - Kvantumrezisztens titkosítás + Kvantumbiztos titkosítás Megpróbálhatja még egyszer. Átköltöztetés befejezve Átköltöztetés egy másik eszközre QR-kód használatával. Átköltöztetés - Megjegyzés: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését.]]> + 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.]]> Megpróbálhatja még egyszer. - Hibás hivatkozás - végpontok közötti kvantumrezisztens titkosítás + Érvénytelen hivatkozás + végpontok közötti kvantumbiztos titkosítás Ez a csevegés végpontok közötti titkosítással védett. Átköltöztetési párbeszédablak megnyitása - Ez a csevegés végpontok közötti kvantumrezisztens tikosítással védett. - végpontok közötti titkosítással és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi.]]> - végpontok közötti kvantumrezisztens titkosítással és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi.]]> - Hiba az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel. + Ez a csevegés végpontok közötti kvantumbiztos titkosítással védett. + 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.]]> + 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.]]> + Hiba történt az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel. Keresse meg ezt az engedélyt az Android beállításaiban, és adja meg kézzel. Engedélyezés a beállításokban Engedély(ek) megadása a hívások kezdeményezéséhez @@ -1721,29 +1677,29 @@ Engedélyek megadása Vezetékes Ethernet Mobilhálózat - Internetkapcsolat + Hálózati kapcsolat Nincs hálózati kapcsolat További Wi-Fi továbbított - A SimpleX hivatkozások küldése le van tiltva - A csoport tagjai küldhetnek SimpleX hivatkozásokat. + A SimpleX-hivatkozások küldése le van tiltva + A tagok küldhetnek SimpleX-hivatkozásokat. tulajdonosok - adminok - minden tag - SimpleX hivatkozás + adminisztrátorok + összes tag + SimpleX-hivatkozások A hangüzenetek küldése le van tiltva - A SimpleX hivatkozások küldése ebben a csoportban le van tiltva. - A SimpleX hivatkozások küldése le van tiltva - Fájlok és média tartalom küldése le van tiltva - A SimpleX hivatkozások küldése engedélyezve van. - Számukra engedélyezve: + A SimpleX-hivatkozások küldése le van tiltva. + A SimpleX-hivatkozások küldése le van tiltva + A fájlok- és médiatartalmak nincsenek engedélyezve + A SimpleX-hivatkozások küldése engedélyezve van. + Számukra engedélyezve mentett - mentve innen: %s - Továbbítva innen: + elmentve innen: %s + Továbbítva innen A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet. Mentett - Mentve innen: + Elmentve innen Letöltés Továbbítás Továbbított @@ -1762,6 +1718,640 @@ Bejövő hívás csengőhangja Az üzenet forrása titokban marad. Profilképek - Profilkép alakzat + Profilkép alakzata Négyzet, kör vagy bármi a kettő között. - \ No newline at end of file + Célkiszolgáló-hiba: %1$s + Továbbítókiszolgáló: %1$s\nHiba: %2$s + Hálózati problémák – az üzenet többszöri elküldési kísérlet után lejárt. + A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal. + Érvénytelen kulcs vagy ismeretlen kapcsolat – valószínűleg ez a kapcsolat törlődött. + Továbbítókiszolgáló: %1$s\nCélkiszolgáló-hiba: %2$s + Hiba: %1$s + Kapacitás túllépés – a címzett nem kapta meg a korábban elküldött üzeneteket. + Üzenetkézbesítési figyelmeztetés + A kiszolgáló címe nem kompatibilis a hálózati beállításokkal. + Soha + Ismeretlen kiszolgálók + Ha az IP-cím rejtett + Üzenet állapotának megjelenítése + Visszafejlesztés engedélyezése + Mindig + Nem + Nem védett + Igen + NE használjon privát útválasztást. + Privát útválasztás + Használjon privát útválasztást ismeretlen kiszolgálókkal. + Mindig használjon privát útválasztást. + Üzenet-útválasztási mód + 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. + 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. + 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. + Üzenet-útválasztási tartalék + PRIVÁT ÜZENET-ÚTVÁLASZTÁS + Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett. + 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. + Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára. + FÁJLOK + IP-cím védelme + Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS-proxy engedélyezve van). + Ismeretlen kiszolgálók! + Tor vagy VPN nélkül az Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára:\n%1$s. + Összes színmód + Fekete + Színmód + Sötét + Sötét mód + Sötét mód színei + Illesztés + Jó napot! + Jó reggelt! + Speciális beállítások + Alkalmazás erre + Csevegés színei + Csevegés témája + Kitöltés + Profiltéma + Csevegési lista megjelenítése új ablakban + Világos + Világos mód + Fogadott válaszüzenet-buborék színe + Kép eltávolítása + Mozaik + Szín visszaállítása + Méretezés + Válaszüzenet-buborék színe + Alapértelmezett téma beállítása + Rendszer + Háttérkép kiemelőszíne + Háttérkép háttérszíne + További kiemelőszín 2 + Alkalmazás téma + Perzsa kezelőfelület + Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben. + Ismeretlen kiszolgálókról származó fájlok megerősítése. + Javított üzenetkézbesítés + Alkalmazás témájának visszaállítása + Tegye egyedivé a csevegéseit! + Új csevegési témák + Privát üzenet-útválasztás 🚀 + Fájlok biztonságos fogadása + Csökkentett akkumulátor-használattal. + 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 + Felhasználó által létrehozott téma visszaállítása + Üzenetsorbaállítási információ + nincs + Kézbesítési hibák felderítése + a kiszolgáló sorbaállítási információi: %1$s\n\nutoljára fogadott üzenet: %2$s + Érvénytelen kulcs vagy ismeretlen fájltöredékcím – valószínűleg a fájl törlődött. + Ideiglenes fájlhiba + Üzenet állapota + Üzenet állapota: %s + Fájlhiba + A fájl nem található – valószínűleg a fájlt törölték vagy visszavonták. + Fájlkiszolgáló-hiba: %1$s + Fájl állapota + Fájl állapota: %s + Másolási hiba + Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén. + Ellenőrizze, hogy a hordozható eszköz és a számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint a számítógép tűzfalában engedélyezve van-e a kapcsolat.\nMinden további problémát osszon meg a fejlesztőkkel. + Nem lehet üzenetet küldeni + A kijelölt csevegési beállítások tiltják ezt az üzenetet. + Próbálja meg később. + A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. + Inaktív tag + Továbbított üzenet + Az üzenet később is kézbesíthető, ha a tag aktívvá válik. + Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja. + Hivatkozás beolvasása / beillesztése + Konfigurált SMP-kiszolgálók + Egyéb SMP-kiszolgálók + Egyéb XFTP-kiszolgálók + letiltva + inaktív + Nagyítás + Információk a kiszolgálókról + Kapcsolódás + Hibák + Függőben + Statisztikagyűjtés kezdete: %s.\nAz összes adat privát módon van tárolva az eszközén. + Elküldött üzenetek + Proxyzott kiszolgálók + Újrakapcsolódik a kiszolgálókhoz? + Újrakapcsolódik a kiszolgálóhoz? + Hiba történt a kiszolgálóhoz való újrakapcsolódáskor + Újrakapcsolódás az összes kiszolgálóhoz + Hiba történt a statisztikák visszaállításakor + Visszaállítás + Az összes statisztika visszaállítása + Visszaállítja az összes statisztikát? + A kiszolgálók statisztikái visszaállnak – ez a művelet nem vonható vissza! + Részletes statisztikák + Letöltve + lejárt + egyéb + Összes fogadott üzenet + Üzenetfogadási hibák + Újrakapcsolódás + Üzenetküldési hibák + Közvetlenül küldött + Összes elküldött üzenet + Proxyn keresztül küldött + SMP-kiszolgáló + Statisztikagyűjtés kezdete: %s. + Feltöltve + XFTP-kiszolgáló + Proxyzott + duplikációk + egyéb hibák + Kapcsolatok + Létrehozva + Biztosítva + Törlési hibák + Méret + Feltöltött fájlok + Letöltött töredékek + Letöltött fájlok + Kiszolgáló-beállítások megnyitása + Kiszolgáló címe + Feltöltési hibák + Visszaigazolt + Visszaigazolási hibák + próbálkozások + Törölt töredékek + Összes profil + Feltöltött töredékek + Elkészült + Kapcsolódott kiszolgálók + Konfigurált XFTP-kiszolgálók + Kapcsolódott + Jelenlegi profil + További részletek + visszafejtési hibák + Törölve + Fogadott üzenetek + Letöltési hibák + Hiba + Hiba történt a kiszolgálókhoz való újrakapcsolódáskor + Fájlok + Betűméret + Nincs információ, próbálja meg újratölteni + Korábban kapcsolódott kiszolgálók + Privát útválasztási hiba + Fogadott üzenetek + 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. + 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. + Elküldött üzenetek + Munkamenetek átvitele + Összes kapcsolat + Statisztikák + Információk megjelenítése a következőhöz + A kiszolgáló verziója nem kompatibilis az alkalmazással: %1$s. + Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál. + Aktív kapcsolatok száma + Üzenetjelentés + Feliratkozva + Feliratkozási hibák + Mellőzött feliratkozások + Százalék megjelenítése + Alkalmazásfrissítés letöltve + Frissítések keresése + Frissítések keresése + Alkalmazásfrissítés letöltése, ne zárja be az alkalmazást + Letöltés – %s (%s) + Sikeresen telepítve + Frissítés telepítése + Fájl helyének megnyitása + Indítsa újra az alkalmazást. + Emlékeztessen később + Ezen verzió kihagyása + 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. + Új verzió érhető el: %s + A frissítés letöltése megszakítva + Béta + Letiltás + Letiltva + Stabil + Hiba történt a(z) %1$s továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbítókiszolgálóval. + A(z) %1$s továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. + A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbítókiszolgáló beállításaival. + Médiatartalom elhomályosítása + Közepes + Kikapcsolva + Enyhe + Erős + A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. + A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s. + hívás + A partner törölve lesz – ez a művelet nem vonható vissza! + Csak a beszélgetés törlése + megnyitás + Beszélgetés törölve! + Partner törölve! + Archivált partnerek + Nincsenek szűrt partnerek + Hivatkozás beillesztése + A hívások le vannak tiltva! + Nem lehet felhívni a partnert + Nem lehet üzenetet küldeni a csoporttagnak + Kapcsolódás a partnerhez, várjon vagy ellenőrizze később! + Törölt partner. + Nem lehet felhívni a csoporttagot + Engedélyezi a hívásokat? + Meghívás + üzenet + Beszélgetés megtartása + Biztosan törli a partnert? + kapcsolódás + Könnyen elérhető alkalmazás-eszköztárak + Törlés értesítés nélkül + Beállítások + keresés + videó + Az „Archivált partnerekből” továbbra is küldhet üzeneteket neki: %1$s. + Partnerek + Kérje meg a partnerét, hogy engedélyezze a hívásokat. + Üzenet küldése a hívások engedélyezéséhez. + Engedélyeznie kell a hívásokat a partnere számára, hogy fel tudják hívni egymást. + A(z) %1$s nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. + Üzenet… + Kijelölés + Az üzenetek az összes tag számára moderáltként lesznek megjelölve. + Nincs semmi kijelölve + Az üzenetek törlésre lesznek jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. + Törli a tagok %d üzenetét? + %d kijelölve + Az üzenetek az összes tag számára törölve lesznek. + Csevegési adatbázis exportálva + Kapcsolatok- és kiszolgálók állapotának megjelenítése. + Kapcsolódjon gyorsabban a partnereihez. + Folytatás + Ellenőrizze a hálózatát + Média- és fájlkiszolgálók + Legfeljebb 20 üzenet egyszerre való törlése. + Védi az IP-címét és a kapcsolatait. + Könnyen elérhető eszköztár + Üzenetkiszolgálók + SOCKS-proxy + Néhány fájl nem lett exportálva + Az exportált adatbázist átköltöztetheti. + Mentés és újrakapcsolódás + Használja az alkalmazást egy kézzel. + A partnerek archiválása a későbbi csevegéshez. + TCP-kapcsolat + Az exportált archívumot elmentheti. + Tippek visszaállítása + Csevegési lista átváltása: + Ezt a „Megjelenés” menüben módosíthatja. + Új médiabeállítások + Lejátszás a csevegési listából. + Elhomályosítás a jobb adatvédelemért. + Automatikus alkalmazás-frissítés + Létrehozás + Új verziók letöltése a GitHubról. + Betűméret növelése. + Meghívás + Új csevegési élmény 🎉 + Új üzenet + Érvénytelen hivatkozás + Ellenőrizze, hogy a SimpleX-hivatkozás helyes-e. + Hiba történt a profilváltáskor + A kapcsolata át lett helyezve ide: %s, de egy váratlan hiba történt a profilra való átirányításkor. + Az üzenetek törölve lesznek – ez a művelet nem vonható vissza! + Eltávolítja az archívumot? + A feltöltött adatbázis-archívum véglegesen el lesz távolítva a kiszolgálókról. + CSEVEGÉSI ADATBÁZIS + Profil megosztása + Rendszerbeállítások használata + Csevegési profil kijelölése + Ne használja a hitelesítőadatokat proxyval. + Különböző proxy-hitelesítőadatok használata az összes profilhoz. + Különböző proxy-hitelesítőadatok használata az összes kapcsolathoz. + Jelszó + Felhasználónév + A hitelesítőadatai titkosítatlanul is elküldhetők. + Hiba történt a proxy mentésekor + Győződjön meg arról, hogy a proxy konfigurációja helyes. + Proxyhitelesítés + Véletlenszerű hitelesítőadatok használata + %1$d egyéb fájlhiba. + Nincs mit továbbítani! + %1$d fájl letöltése még folyamatban van. + Nem sikerült letölteni %1$d fájlt. + %1$d fájl nem lett letöltve. + Letöltés + %1$d fájl törölve lett. + %1$s üzenet továbbítása + Üzenetek továbbítása… + %1$d fájlhiba:\n%2$s + %1$s üzenet nem lett továbbítva + Továbbít %1$s üzenetet? + Továbbítja az üzeneteket fájlok nélkül? + Az üzeneteket törölték miután kijelölte őket. + %1$s üzenet mentése + Hiba történt az üzenetek továbbításakor + Hang elnémítva + 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 + Sarok + Üzenetbuborék alakja + Farok + Kiszolgáló + Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítő-adatokat fog használni. + Alkalmazás munkamenete + Az összes kiszolgálóhoz új, SOCKS-hitelesítő-adatok legyenek használva. + Kattintson a címmező melletti info gombra a mikrofon használatának engedélyezéséhez. + 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. + 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. + Továbbfejlesztett hívásélmény + Továbbfejlesztett üzenetdátumok. + Továbbfejlesztett felhasználói élmény + Személyre szabható üzenetbuborékok. + Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása. + Legfeljebb 20 üzenet egyszerre való továbbítása. + Hang/Videó váltása hívás közben. + Csevegési profilváltás az egyszer használható meghívókhoz. + Továbbfejlesztett biztonság ✅ + A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva. + Hiba történt a kiszolgálók mentésekor + Nincsenek üzenet-kiszolgálók. + Nincsenek üzenetfogadási kiszolgálók. + Nincsenek média- és fájlkiszolgálók. + A(z) %s nevű csevegési profilhoz: + Cím vagy egyszer használható meghívó? + Új kiszolgáló + Címbeállítások + Előre beállított kiszolgálók + Üzemeltető + Feltételek megtekintése + Nincsenek kiszolgálók a privát üzenet-útválasztáshoz. + Nincsenek fájlküldő-kiszolgálók. + Nincsenek fájlfogadási kiszolgálók. + Hibák a kiszolgálók konfigurációjában. + Hiba történt a feltételek elfogadásakor + Kézbesítetlen üzenetek + A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van. + Nincs üzenet + Ez az üzenet törölve lett vagy még nem érkezett meg. + Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. + Cím nyilvános megosztása + SimpleX-cím megosztása a közösségi médiában. + Egyszer használható meghívó megosztása egy baráttal + csak egyetlen partnerrel használható – személyesen vagy bármilyen üzenetváltó-alkalmazáson keresztül megosztható.]]> + Beállíthatja a partner nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást. + Kapcsolatbiztonság + A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó-alkalmazáson keresztül. + A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat a partnerével. + A közösségi médiához + Vagy a privát megosztáshoz + SimpleX-cím vagy egyszer használható meghívó? + Egyszer használható meghívó létrehozása + Kiszolgálóüzemeltetők + Hálózatüzemeltetők + Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ. + 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. + Jelölje ki a használni kívánt hálózatüzemeltetőket. + Felülvizsgálat később + A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja. + A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltetők számára. + Az üzemeltetőket a „Hálózat és kiszolgálók” menüben konfigurálhatja. + Frissítés + Folytatás + Feltételek felülvizsgálata + Elfogadott feltételek + A feltételek automatikusan el lesznek fogadva az engedélyezett üzemeltetők számára a következő időpontban: %s. + Saját kiszolgálók + %s.]]> + %s.]]> + %s kiszolgáló + Hálózatüzemeltető + Weboldal + Feltételek elfogadásának ideje: %s. + A feltételek el lesznek elfogadva a következő időpontban: %s. + Kiszolgálók használata + %s használata + 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: + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + Feltételek elfogadása + Használati feltételek + %s kiszolgálóinak használatához fogadja el a használati feltételeket.]]> + Használat az üzenetekhez + A fogadáshoz + A privát útválasztáshoz + Hozzáadott üzenetkiszolgálók + Használat a fájlokhoz + A küldéshez + Hozzáadott média- és fájlkiszolgálók + Feltételek megnyitása + Módosítások megtekintése + Hiba történt a kiszolgáló frissítésekor + A kiszolgáló-protokoll módosult. + A kiszolgáló üzemeltetője módosult. + Kiszolgáló-üzemeltető + Kiszolgáló hozzáadva a következő üzemeltetőhöz: %s. + Hiba történt a kiszolgáló hozzáadásakor + Átlátszóság + Elhomályosítás + Hálózati decentralizáció + A második előre beállított üzemeltető az alkalmazásban! + 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. + Alkalmazás-eszköztárak + a metaadatok jobb védelme érdekében. + Javított csevegési navigáció + - Csevegés megnyitása az első olvasatlan üzenetnél.\n- Ugrás az idézett üzenetekre. + Frissített feltételek megtekintése + A jelenlegi csevegési profiljához tartozó új fájlok kiszolgálói + Vagy archívumfájl importálása + Távoli hordozható eszközök + Xiaomi eszközök: engedélyezze az automatikus indítást a rendszerbeállításokban, hogy az értesítések működjenek.]]> + A küldéshez másolhatja és csökkentheti az üzenet méretét. + Adja hozzá a munkatársait a beszélgetésekhez. + Üzleti cím + végpontok közötti titkosítással, a közvetlen üzenetek továbbá kvantumbiztos titkosítással is rendelkeznek.]]> + Hogyan segíti az adatvédelmet + Nincs háttérszolgáltatás + Értesítések és akkumulátor + Az alkalmazás mindig fut a háttérben + Elhagyja a csevegést? + Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. + Csevegés törlése + Meghívás a csevegésbe + Barátok hozzáadása + Munkatársak hozzáadása + A csevegés minden tag számára törölve lesz – ez a művelet nem vonható vissza! + A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza! + Törli a csevegést? + Csevegés elhagyása + Csak a csevegés tulajdonosai módosíthatják a csevegési beállításokat. + Könnyen elérhető csevegési eszköztár + A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza! + Csevegés + A tag szerepköre a következőre fog módosulni: „%s”. A csevegés összes tagja értesítést fog kapni. + A csevegési profilja el lesz küldve a csevegésben résztvevő tagok számára + A tagok közötti közvetlen üzenetek le vannak tiltva. + Üzleti csevegések + Saját ügyfeleinek adatvédelme. + %1$s.]]> + A csevegés már létezik! + Csökkentse az üzenet méretét, és küldje el újra. + Üzenetek ellenőrzése 10 percenként + Az üzenet túl nagy! + Csökkentse az üzenet méretét vagy távolítsa el a médiát, és küldje el újra. + A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben. + Amikor egynél több üzemeltető van engedélyezve, akkor egyik sem rendelkezik olyan metaadatokkal, amelyekből megtudható, hogy ki kivel kommunikál. + elfogadott meghívó + Függőben lévő meghívási kérelem + Az üzemeltetőkről + 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. + A titkosítás újraegyeztetése folyamatban van. + A kapcsolat titkosítása újraegyeztetést igényel. + Javítás + Kapcsolat javítása? + Naplózás engedélyezése + Hiba történt az adatbázis mentésekor + áthúzott + A következő csevegési profil törlése + Üzenetek törlése ennyi idő után + a + b + A kapcsolat nem áll készen. + Megnyitás a következővel: %s + Lista + Csoportok + Lista hozzáadása + Összes + Hozzáadás listához + Hiba történt a csevegési lista létrehozásakor + Hiba történt a csevegési lista betöltésekor + Hiba történt a csevegési lista frissítésekor + Üzleti + Partnerek + Kedvencek + Nincsenek csevegések + Nem találhatók csevegések + Nincsenek olvasatlan csevegések + Lista létrehozása + Lista mentése + Az összes csevegés el lesz távolítva a következő listáról, és a lista is törlődik: %s + Törlés + Törli a listát? + Szerkesztés + Lista neve… + Az összes lista nevének és emodzsijának különbözőnek kell lennie. + Nincsenek csevegések a(z) %s nevű listában. + Jegyzetek + Lista módosítása + Elrendezés módosítása + Hiba történt a jelentés létrehozásakor + Hiba történt a beállítások mentésekor + A jelentés archiválva lesz az Ön számára. + Tartalom jelentése: csak a csoport moderátorai látják. + Archívum + Archiválja a jelentést? + archivált jelentés + moderátor + Közösségi irányelvek megsértése + Kéretlen tartalom jelentése: csak a csoport moderátorai látják. + Csak a küldő és a moderátorok látják + Csak Ön és a moderátorok látják + Jelentés indoklása? + Kéretlen tartalom + Egyéb indoklás + Kifogásolt tartalom + Kifogásolt profil + Jelentés + Tag profiljának jelentése: csak a csoport moderátorai látják. + Egyéb jelentés: csak a csoport moderátorai látják. + Szabálysértés jelentése: csak a csoport moderátorai látják. + Jelentés archiválása + Jelentés törlése + Tagok jelentései + 1 jelentés + Jelentések + %s által archivált jelentés + %d jelentés + Kéretlen tartalom + A tartalom sérti a használati feltételeket + A kapcsolat le van tiltva + A kiszolgáló üzemeltetője letiltotta a fájlt:\n%1$s. + A kiszolgáló üzemeltetője letiltotta a kapcsolatot:\n%1$s. + Mindig kérdezzen rá + Igen + Hivatkozás megnyitása + Hivatkozás megnyitása a csevegési listából + Nem + Megnyitja a webhivatkozást? + Csevegés nevének beállítása… + Letiltja az automatikus üzenettörlést? + Üzenetek törlésének letiltása + Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve. + 1 év + alapértelmezett (%s) + Csevegési üzenetek törlése a saját eszközéről. + Módosítja az automatikus üzenettörlést? + 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. + A következő TCP-port használata, amikor nincs port megadva: %1$s. + TCP-port az üzenetváltáshoz + Webport használata + Olvasatlan említések + Összes némítása + Legfeljebb %1$s tagot említhet meg egy üzenetben! + Az üzenetek jelentése a moderátorok felé engedélyezve van. + Az üzenetek a moderátorok felé történő jelentésének megtiltása. + Archiválja az összes jelentést? + Archivál %d jelentést? + Csak magamnak + Jelentések archiválása + Az összes moderátor számára + Az üzenetek jelentése ebben a csoportban le van tiltva. + A tagok jelenthetik az üzeneteket a moderátorok felé. + Az összes jelentés archiválva lesz az Ön számára. + Jelentés: %s + Tagok említése 👋 + Privát jelentések küldése + Ne maradjon le a fontos üzenetekről. + Gyorsabb üzenetküldés. + Gyorsabb csoporttörlés. + Segítsen az adminisztrátoroknak a csoportjaik moderálásában. + Privát nevek a médiafájlokhoz. + Üzenetek eltűnési idejének módosítása a csevegésekben. + Továbbfejlesztett, gyorsabb csoportok + Továbbfejlesztett adatvédelem és biztonság + Kapjon értesítést, ha megemlítik. + Csevegések listákba szervezése + elutasítva + elutasítva + 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. + függőben + jóváhagyásra vár + Hiba történt az adatbázis-jelmondat olvasásakor + 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. + Frissített feltételek + A tagok el lesznek távolítva a csoportból – ez a művelet nem vonható vissza! + Eltávolítja a tagokat? + Az összes tag számára letiltja ezeket a tagokat? + A tagok el lesznek távolítva a csevegésből – ez a művelet nem vonható vissza! + Az összes tag számára feloldja a tagok letiltását? + Ezen tagok összes új üzenete el lesz rejtve! + A tagok összes üzenete meg fog jelenni! + moderátorok + Elfogadás + A SimpleX Chat használatával Ön elfogadja, hogy:\n- csak elfogadott tartalmakat tesz közzé a nyilvános csoportokban.\n- tiszteletben tartja a többi felhasználót, és nem küld kéretlen tartalmat senkinek. + Adatvédelmi szabályzat és felhasználási feltételek. + A privát csevegések, a csoportok és a partnerek nem érhetők el a szerver üzemeltetői számára. + Kiszolgálóüzemeltetők beállítása + Nem támogatott kapcsolattartási hivatkozás + Rövid hivatkozások használata (béta) + Rövid hivatkozás + Teljes hivatkozás + 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. + SimpleX-csatornahivatkozás + Összes kiszolgáló + Kikapcsolva + Előre beállított kiszolgálók + A 443-as TCP-port használata kizárólag az előre beállított kiszolgálokhoz. + 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 index 9680161c4f..07f5645f79 100644 --- 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 @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ + + + + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 index d3052a6c84..78d1b86471 100644 --- 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 @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 index 53bc5becaa..d39581566e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ + + + + + + + + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - + Created with Fabric.js 5.3.0 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ 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 @@ - \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ + \ 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 index f4c284a5f2..ab47ff4136 100644 --- 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 @@ -1,8 +1,8 @@ \ No newline at end of file + \ 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 index f1aba5bee6..7f2fb6e38e 100644 --- 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 @@ -1 +1 @@ - \ No newline at end of file + \ 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 index 0f556da867..5271a02284 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg @@ -1 +1 @@ - \ No newline at end of file + \ 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 index 7ad0b14b70..174b80bcc7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ + \ 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 index 2bda1e9d74..8487893d8d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ + + + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ + + + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + + + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 index 766dc76be8..9e127f18f8 100644 --- 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 @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ + + + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ + \ 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 @@ + \ 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 index 513bb38c40..48fe86d1ec 100644 --- 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 @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ + + + + + + \ 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 @@ + + + + + + \ 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 @@ + + + + + + \ 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 @@ + + + + + + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 index 8695857b97..c4c54f0e02 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 index 4bd8e90cb6..2e806eb315 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg @@ -1 +1 @@ - \ No newline at end of file + \ 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 index 368e2fcc1f..b7afaf420b 100644 --- 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 @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ No newline at end of file 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ 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 @@ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + + + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + + + \ 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 @@ - \ No newline at end of file + \ 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 index 068bfc1a82..0663015ae4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg @@ -1 +1 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ - \ No newline at end of file + \ 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 @@ + \ 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 @@ + \ 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 @@ + + + + + + + + \ 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 @@ + + + %1$s ANGGOTA + Alamat + %1$d pesan dimoderasi oleh %2$s + 1 menit + 30 detik + 5 menit + Semua pesan akan dihapus - ini tidak bisa dikembalikan! Pesan akan HANYA dihapus untukmu. + Terima permintaan koneksi? + Tentang alamat SimpleX + Tambah kontak + Tentang SimpleX + %1$d pesan gagal terdekripsi. + Mohon laporkan ke pengembang. + 1 bulan + 1 minggu + Terima + Anda sudah bergabung dalam grup <b>%1$s</b>. + Panggilan suara + Batal + Izinkan pesan suara? + Terima + Versi aplikasi + Versi aplikasi: v%s + panggilan suara + 1 hari + Buat pesan sambutan + Batalkan penggantian alamat + Tambah profil + Corak + Tambahan sekunder + Tema aplikasi + selalu + Izinkan mengirim pesan suara. + semua anggota + Panggilan suara dan video + Beberapa hal lainnya + Sudah terhubungkan! + Sudah bergabung dengan grup! + %1$s ingin menghubungimu lewat + Batalkan penggantian alamat? + diatas, lalu: + Tambahkan ke perangkat lain + Boleh + Selalu + APLIKASI + Tampilan + Tentang SimpleX Chat + Terima + Terima + Batalkan + Kamera + tebal + memanggil… + Bluetooth + Panggilan diakhiri + Ubah + <b>Peringatan</b>: arsip akan dihapus. + <b>Buat grup</b>: untuk membuat grup baru. + Blokir anggota + Blokir + Kamera + Hitam + Blokir anggota? + Ubah alamat penerima + Beta + Blokir untuk semua + Blokir anggota untuk semua? + Panggilan telah ditutup! + Seluler + Hubungkan melalui tautan satu kali? + Gabung Grup? + Gunakan profil saat ini + Gunakan profil penyamaran baru + Profil Anda akan dikirim ke kontak yang menerima tautan ini. + Anda akan terhubung ke semua anggota grup. + Hubungkan + Hubungkan penyamaran + Membuka basis data… + k + Hubungkan melalui alamat kontak? + Bersihkan + Bersihkan + Bersihkan + Cek koneksi internetmu dan coba lagi + Hubungkan + terhubung + Hubungkan + Terhubung + Terhubung + terhubung + Hubungkan langsung? + tersambung + selesai + terhubung + Komputer yang terhubung + Terhubung + Selesai + Ponsel yang terhubung + Hubungkan secara otomatis + menyambungkan… + Menghubungkan + Tautan tidak valid + Periksa apakah tautan SimpleX sudah benar. + Anda terhubung ke server yang digunakan untuk menerima pesan dari kontak ini. + Mencoba menyambung ke server yang digunakan untuk menerima pesan dari kontak ini (error: %1$s). + Migrasi basis data sedang berlangsung, +\nmemerlukan waktu beberapa menit. + menghubungkan + Lokasi file tidak valid + Tampilan macet + Anda membagikan lokasi file yang tidak valid. Laporkan masalah ini ke pengembang aplikasi. + error + Tautan sekali + %1$d pesan yang terlewati + %1$d pesan yang dilewati + Versi perangkat <b>%s</b> tidak didukung. Harap pastikan kamu menggunakan versi yang sama pada kedua perangkat. + Perangkat <b>%s</b> sedang sibuk + Perangkat <b>%s</b> tidak terhubung + Perangkat <b>%s</b> tidak aktif + Perangkat <b>%s</b> tidak di temukan + Terima penyamaran + panggilan diterima + 6 bahasa antarmuka baru + Perangkat <b>%s</b> tidak terhubung + Tidak ada kode sandi aplikasi + Belum ada koneksi langsung, pesan diteruskan oleh admin. + Tidak ada obrolan yang difilter + Tidak ada yang dipilih + Hanya 10 gambar dapat dikirim pada saat bersamaan + Notifikasi + Hanya menghapus percakapan + Koneksi jaringan yang lebih handal. + Pengalaman obrolan yang baru 🎉 + Pilihan media baru + Izinkan panggilan? + Pesan baru + Hanya kamu yang dapat mengirim pesan menghilang. + Tidak ada pengidentifikasi pengguna. + Hanya pemilik grup yang dapat mengubah preferensi grup. + dan %d peristiwa lainnya + Peran baru anggota + Tidak ada kontak di pilih + Tidak ada kontak untuk ditambahkan + Tidak ada teks + Semua pesan baru dari %s akan disembunyikan! + tidak ada + Status jaringan + Seluruh obrolan dan pesan akan dihapus - ini tidak bisa dibatalkan! + Tidak ada perangkat terkoneksi + Tidak ada panggilan latar + Pratinjau notifikasi + Layanan notifikasi + Selalu aktif + Permintaan kontak baru + Pesan baru + Kemungkinan besar kontak ini telah menghapus koneksi dengan kamu. + Masalah jaringan - pesan kadaluwarsa setelah beberapa kali mencoba mengirim. + Tidak ada obrolan dipilih + Hanya pemilik grup yang dapat mengaktifkan file dan media. + (hanya disimpan oleh anggota grup) + Lagi + Tautan undangan satu kali + Obrolan baru + Jaringan & server + Pengaturan tingkat lanjut + Host Onion akan diperlukan untuk koneksi. +\nHarap diperhatikan: Anda tidak akan dapat terhubung ke server tanpa alamat .onion. + Host Onion tidak akan digunakan. + Selalu gunakan perutean pribadi. + Pemberitahuan akan berhenti bekerja sampai kamu meluncurkan ulang aplikasi + Semua kontak Anda akan tetap terhubung. Pembaruan profil akan dikirim ke kontak Anda. + Tidak ada enkripsi ujung-ujung + Kode sandi baru + Mati + Seluruh data aplikasi dihapus. + Arsip database baru + Arsip database lama + tidak pernah + Tidak ada file yang diterima atau dikirim + Menyetujui enkripsi… + Bisukan ketika tidak aktif! + Seluruh mode warna + mati` + tidak + on + mati + Izinkan kontak kamu menghapus pesan terkirim secara permanen. (24 jam) + Izinkan penghapusan pesan yang tidak dapat diubah hanya jika kontak kamu mengizinkannya. (24 jam) + Izinkan kontak kamu mengirim pesan suara. + Hanya kamu yang dapat menghapus pesan secara permanen (kontak kamu dapat menandainya untuk dihapus). (24 jam) + Hanya kontak kamu yang dapat menghapus pesan secara permanen (kamu dapat menandainya untuk dihapus). (24 jam) + Hanya kamu yang dapat menambahkan reaksi pesan. + Hanya kamu yang dapat melakukan panggilan. + Hanya kontak kamu yang dapat menambahkan reaksi pesan. + Izinkan pengiriman pesan langsung ke anggota. + Izinkan untuk mengirim pesan menghilang. + ditawarkan %s: %2s + Beberapa profil obrolan + Lebih banyak peningkatan akan segera hadir! + Tidak ada admin yang dapat: +\n- menghapus pesan anggota. +\n- menonaktifkan anggota (peran “pengamat”) + Lebih banyak peningkatan akan segera hadir! + - pengiriman pesan yang lebih stabil. +\n- group yang sedikit lebih baik. +\n- dan lainnya! + Aplikasi desktop baru! + Manajemen jaringan + bulan + Semua kontak, percakapan, dan file kamu akan dienkripsi dengan aman dan diunggah dalam beberapa bagian ke relay XFTP yang telah dikonfigurasi. + Tidak ada koneksi jaringan + Bisu + Koneksi jaringan + TIdak pernah + Mati + Hanya 10 video dapat dikirim pada saat bersamaan + Hanya perangkat klien yang menyimpan profil pengguna, kontak, grup, dan pesan. + Hanya pemilik grup yang dapat mengaktifkan pesan suara. + Seluruh pesan akan dihapus - ini tidak bisa dibatalkan! + Izinkan turun versi + Tidak ada informasi pengiriman + Boleh + OK + Tidak ada rincian + Tautan undangan satu kali + Host Onion akan di pakai jika tersedia. + Selalu gunakan relay + Nama tampilan baru: + Frasa sandi baru + Pemberitahuan akan dikirim hanya sampai saat aplikasi berhenti! + Pengaturan tingkat lanjut + Izinkan reaksi pesan hanya jika kontak Anda mengizinkannya. + Izinkan pesan suara hanya jika kontak kamu mengizinkannya. + Izinkan kontak kamu menambahkan reaksi pesan. + Izinkan kontak kamu untuk menghubungimu. + Izinkan kontak kamu untuk mengirim pesan menghilang. + Izinkan panggilan hanya jika kontak kamu mengizinkannya. + Izinkan pesan menghilang hanya jika kontak kamu mengizinkannya. + Izinkan reaksi pesan. + Izinkan untuk menghapus pesan terkirim secara permanen. (24 jam) + Bisu + Tidak ada riwayat + Tema obrolan yang baru + Semua profil + Izinkan untuk mengirim tautan SimpleX. + Semua data akan terhapus jika ini dimasukkan. + Tidak kompatibel! + Perangkat seluler baru + Hanya satu perangkat yang dapat bekerja pada saat bersamaan + Tidak + Tidak ada kontak yang di filter + pengamat + menyetujui enkripsi untuk %s… + Semua anggota grup akan tetap terhubung. + Tidak ada info, coba muat ulang + Tidak + Baru di %s + ditawarkan %s + Hanya kamu yang dapat mengirim pesan suara. + Hanya kontak kamu yang dapat melakukan panggilan. + Izinkan untuk mengirim file dan media. + Semua kontak kamu akan tetap terhubung. + %1$d berkas telah dihapus. + Corak tambahan + Corak tambahan 2 + %1$s pesan tidak diteruskan + Koneksi aktif + admin + %1$d berkas galat :\n%2$s + Balas + Bagikan + Salin + Edit + Info + Tersimpan + Cari + Diteruskan + Disimpan dari + Perluas + Sedang + Hapus pesan? + Hapus %d pesan? + Pesan akan dihapus - Tindakan ini tidak dapat dibatalkan! + %1$d berkas masih diunduh. + Jawab panggilan + Pesan terkirim + Pesan diterima + Anggota tidak aktif + Pesan diteruskan + Cabut berkas? + Terlalu banyak gambar! + cari + panggilan + PENGATURAN + Untuk semua orang + Hentikan berkas + Cabut berkas + Mengirim berkas akan dihentikan. + Berhenti + Pengaturan + Berhenti kirim berkas? + Penerimaan berkas akan dihentikan. + Gambar disimpan ke Galeri + video + Kontak + %d pesan dihapus + dihapus + Mencoba terhubung ke server untuk menerima pesan dari kontak ini. + disimpan + diundang untuk terhubung + Deskripsi + Nama tampilan duplikat! + Mengirim + Diteruskan dari + Berhenti menerima berkas? + Tarik + Berkas akan dihapus dari server. + terkirim + Teruskan + Unduh + diedit + Obrolan + Selamat Datang! + Chat dengan pengembang + Setel nama kontak… + Pengaturan + Simpan + Kirim pesan langsung untuk terhubung + Berkas disimpan + Alamat server tujuan %1$s tidak kompatibel dengan pengaturan server penerusan %2$s. + Riwayat + Hapus + Perlihat + Sembunyikan + Grup kecil (maks 20) + Pilih + Hapus untuk saya + Gagal meneruskan pesan + %1$d berkas lainnya gagal. + Kontak sudah ada + menghubungkan… + kirim pesan langsung + Anda tidak memiliki obrolan + Memuat obrolan… + Ketuk untuk Hubungkan + Terhubung dengan %1$s? + Teruskan %1$s pesan? + Tidak ada yang diteruskan! + Teruskan pesan tanpa berkas? + Pesan dihapus setelah Anda memilihnya. + %1$d berkas tidak diunduh. + Unduh + Teruskan pesan… + Meneruskan %1$s pesan + Tautan SimpleX tidak diizinkan + Berkas dan media tidak diizinkan + Video + Menunggu video + Berkas tidak ditemukan + Tolak + Bagikan profil + Pilih profil obrolan + Kontak arsipan + Kode yang Anda pindai bukan kode QR tautan SimpleX. + Untuk memverifikasi enkripsi end-to-end dengan kontak Anda, bandingkan (atau pindai) kode pada perangkat Anda. + Kontak Anda + Pindai kode + Tandai terverifikasi + %s telah terverifikasi + %s belum terverifikasi + Bersihkan verifikasi + Konsol obrolan + Server pesan + Server SMP + Beri nilai aplikasi + Bintang di GitHub + Gunakan server SimpleX Chat? + Server SMP Anda + Server XFTP + Kredensial Anda mungkin dikirim tidak terenkripsi. + Username + Server XFTP Anda + Gunakan server SimpleX Chat. + Gunakan kredensial proxy yang berbeda untuk setiap koneksi. + Kata sandi + port %d + Host + Port + Gunakan proxy SOCKS? + Akses server melalui proxy SOCKS pada port %d? Proxy harus dimulai sebelum mengaktifkan opsi ini. + Gunakan koneksi Internet langsung? + Pastikan konfigurasi proxy sudah benar. + Gagal simpan proxy + Saat tersedia + Diperlukan + Jika Anda konfirmasi, server perpesanan akan dapat melihat alamat IP Anda, dan penyedia Anda - server mana yang Anda hubungkan. + Profil obrolan + Koneksi + Server tak dikenal + Kirim pesan secara langsung ketika alamat IP dilindungi dan server Anda atau tujuan tidak mendukung routing pribadi. + Kirim pesan secara langsung ketika server Anda atau server tujuan tidak mendukung routing pribadi. + Tampilkan status pesan + Terima otomatis + Undang teman + Buat + Nama tidak valid! + Buat profil + Masukkan nama Anda: + rahasia + miring + berwarna + menghubung panggilan… + panggilan berakhir %1$s + panggilan berlangsung + berakhir + Headphone + Kesalahan saat menginisialisasi WebView. Perbarui sistem Anda ke versi baru. Mohon hubungi pengembang.\nKesalahan: %s + Anda pilih siapa yang dapat terhubung. + Kebal terhadap spam + Buat profil Anda + Buat koneksi pribadi + Lewati + Panggilan Anda + Lihat + Server relai hanya digunakan jika diperlukan. Pihak lain dapat mengamati alamat IP Anda. + Speaker mati + Video nyala + Suara mati + Suara nyala + Speaker nyala + Balik kamera + Panggilan berlangsung + Menghubungkan panggilan + Pesan yang terlewati + Hash dari pesan sebelumnya berbeda.\" + Privasi & keamanan + Enkripsi berkas lokal + Terima gambar otomatis + Lindungi layar aplikasi + Lindungi alamat IP + Lihat pesan terakhir + Kirim pratinjau tautan + Draf pesan + Konfirmasi kode sandi + Kunci setelah + Kirim + Kode sandi salah + Kode sandi aplikasi + Kode sandi + %s detik + Ketuk untuk gabung + diblokir %s + Buka + enkripsi aman + status tidak diketahui + diundang + admin + dihapus + grup dihapus + Gagal impor tema + Impor tema + Pastikan berkas memiliki sintaksis YAML yang benar. Ekspor tema untuk mendapatkan contoh struktur berkas tema. + Ekspor tema + Sekunder + Latar + Pesan terkirim + Reset ke tema pengguna + Pasang tema bawaan + Terapkan + Sesuai + Reset ke tema aplikasi + Izin Anda + Izin kontak + bawaan (%s) + Pesan sementara + Pesan pribadi + Pesan suara + Setel preferensi grup + Preferensi Anda + Hapus untuk semua orang + diterima, dilarang + Pasang 1 hari + Kontak dapat menandai pesan untuk dihapus; Anda akan dapat melihatnya. + Melarang pengiriman pesan suara. + Hapus pesan tidak dapat dibatalkan dilarang dalam obrolan ini. + Panggilan audio/video dilarang. + Reaksi pesan dilarang. + Anggota dapat mengirim pesan sementara. + Pesan sementara dilarang. + %d minggu + %d minggu + Keamanan SimpleX Chat diaudit oleh Trail of Bits. + Tautan grup + Admin dapat membuat tautan untuk gabung ke grup. + Maks 40 detik, diterima secara instan. + Hapus pesan tidak dapat dibatalkan + Penerima melihat pesan langsung saat Anda mengetik. + Isolasi transport + Pesan sambutan grup + Antarmuka bahasa Cina dan Spanyol + Video dan berkas hingga 1GB + Terima kasih kepada pengguna – berkontribusi melalui Weblate! + Reaksi pesan + Jaga koneksi Anda + Grup lebih baik + Grup samaran + Melalui protokol quantum resistant yang aman. + Gabung lebih cepat dan pesan lebih handal. + Untuk sembunyikan pesan tak diinginkan. + Gunakan aplikasi saat dalam panggilan. + Konfirmasi berkas dari server tak dikenal. + Tanda terima pengiriman dimatikan! + kustom + Verifikasi kode dengan desktop + Fitur ini belum didukung. Coba pada versi berikutnya. + Kesalahan internal + Mengunduh arsip + Unduhan gagal + Buat tautan arsip + Mulai obrolan + Pastikan Anda mengingat frasa sandi basis data untuk memindahkan. + Profil saat ini + Buat + Reset warna + kirim berkas belum didukung + data tidak valid + gagal menampilkan pesan + gagal menampilkan konten + Gagal dekripsi + Gagal negosiasi ulang enkripsi + Pesan, berkas dan panggilan dilindungi oleh <b>enkripsi end-to-end</b> dengan perfect forward secrecy, penolakan dan pemulihan pembobolan. + Via peramban + Gagal mengganti profil + Buka tautan di peramban mengurangi privasi dan keamanan koneksi. Tautan SimpleX tidak tepercaya akan berwarna merah. + Gagal simpan server SMP + Pastikan alamat server SMP dalam format yang benar, pisahkan baris dan tidak terduplikasi. + Pastikan alamat server XFTP dalam format yang benar, pisahkan baris dan tidak terduplikasi. + gagal terkirim + belum dibaca + Pesan dapat disampaikan kemudian jika anggota menjadi aktif. + Selamat Datang %1$s! + %d Dipilih + Terlalu banyak video! + Pesan suara + pesan + buka + Sistem + setel alamat kontak baru + Tema gelap + Tema + Mode warna + Balasan terkirim + Pesan diterima + Perbesar + Melarang kirim pesan sementara. + Reaksi pesan dilarang. + Panggilan suara/video dilarang. + Anda dan kontak Anda dapat menghapus pesan terkirim secara permanen. (24 jam) + Pesan sementara dilarang dalam obrolan ini. + Pesan suara dilarang dalam obrolan ini. + Anda dan kontak dapat menambahkan reaksi pesan. + Anggota dapat hapus pesan terkirim secara permanen. (24 jam) + Anggota dapat mengirim pesan suara. + Hapus pesan yang tidak dapat dibatalkan dilarang. + Pesan pribadi antar anggota dilarang di grup ini. + Anggota dapat kirim tautan SimpleX. + %d jam + %d jam + %d hari + %d hari + %dmg + %dj + dibatalkan %s + Dengan pesan sambutan opsional. + Terima permintaan kontak secara otomatis + Selengkapnya + Penilaian keamanan + Konfigurasi server ditingkatkan + Tambah server dengan pindai kode QR. + Peningkatan privasi dan keamanan + Terima kasih kepada pengguna – berkontribusi melalui Weblate! + Antarmuka Prancis + Bandingkan kode keamanan dengan kontak Anda. + Simpan draf pesan terakhir, dengan lampiran. + Berdasarkan profil obrolan (bawaan) atau berdasarkan koneksi (BETA). + Untuk melindungi zona waktu, berkas gambar/suara menggunakan UTC. + Penggunaan baterai semakin sedikit + Atur pesan yang ditampilkan kepada anggota baru! + Pindahan data aplikasi + Terima berkas dengan aman + Buat obrolan Anda terlihat berbeda! + Peningkatan pengiriman pesan + UI Lituania + Arsip kontak untuk mengobrol nanti. + Gunakan aplikasi dengan satu tangan. + Terhubung dengan teman lebih cepat. + Ini melindungi alamat IP dan koneksi Anda. + Unduh versi baru dari GitHub. + Nama perangkat ini + Koneksi terputus + Desktop tidak aktif + Ingin bergabung lagi? + Mengimpor arsipan + Pindah perangkat + Gagal Ekspor basis data obrolan + Informasi server + Lihat info untuk + Galat + Diunggah + Diunduh + Pesan terkirim + Statistik server akan direset - ini tidak dapat dibatalkan! + Perbesar ukuran font. + Sumber pesan tetap pribadi. + Siapa pun dapat menjadi pemegang server. + Terdesentralisasi + Kesalahan saat menginisialisasi WebView. Pastikan Anda telah menginstal WebView dan arsitektur yang didukung adalah arm64.\nKesalahan: %s + Gunakan obrolan + Bagaimana caranya + Cara kerja SimpleX + Berkala + Panggilan suara masuk + panggilan suara terenkripsi e2e + panggilan video terenkripsi e2e + Gunakan frasa sandi acak + Panggilan suara & video + Tolak + panggilan video + kontak memiliki enkripsi e2e + Tutup + Video mati + Suara dibisukan + Panggilan tertunda + Aktifkan kunci + Tanpa Tor atau VPN, alamat IP Anda akan terlihat oleh server file. + Mode kunci + Autentikasi dibatalkan + Ubah mode kunci + Kode sandi dipasang! + Kode sandi diubah! + Kode sandi hapus otomatis diubah! + Kode sandi hapus otomatis + Aktifkan hapus otomatis + Pasang kode sandi + BANTUAN + DUKUNG SIMPLEX CHAT + PANGGILAN + Mulai ulang aplikasi untuk buat profil obrolan baru. + Hapus pesan + keluar + Anda keluar + Terang + Warna mode gelap + Reset warna + Mode gelap + Mode terang + Preferensi obrolan + Preferensi kontak + diaktifkan + diaktifkan untuk anda + \nTersedia di v5.1 + Kirim pesan suara tidak diizinkan. + Hingga 100 pesan terakhir dikirim ke anggota baru. + Pesan suara + Pesan sementara + Pesan langsung + Profil obrolan tersembunyi + Moderasi grup + Kustomisasi dan bagikan warna tema. + Perbaiki enkripsi setelah memulihkan cadangan. + Bahasa Arab, Bulgaria, Finlandia, Ibrani, Thailand, dan Ukraina - terima kasih kepada pengguna dan Weblate. + Gabung ke percakapan grup + Bilah pencarian menerima tautan undangan. + Teruskan dan simpan pesan + Panggilan gambar-dalam-gambar + Persegi, lingkaran, atau apa pun di antaranya. + Bentuk gambar profil + Putuskan desktop? + Segarkan + Acak + Buka port di firewall + Hubungkan via tautan? + Kesalahan besar + Gambar + pesan + Menunggu gambar + Diminta untuk menerima gambar + Gambar terkirim + Membuat tautan… + Kode QR tidak valid + Kesalahan saat mengganti profil + Ketuk untuk tempel tautan + Koneksi Anda dipindahkan ke %s tetapi terjadi kesalahan tak terduga saat mengarahkan Anda ke profil. + Kirim kami email + Kirim pertanyaan dan ide + Kunci SimpleX + Bantuan Markdown + Server media & berkas + Server Anda + Alamat server ditetapkan + Gunakan di koneksi baru + Simpan server? + Server untuk koneksi baru profil obrolan Anda saat ini + Gunakan host .onion + Proxy SOCKS + Bagaimana + Konfigurasi server ICE + Server ICE (satu per baris) + Bagikan tautan + Simpan pengaturan? + Buat profil + Simpan kata sandi profil + Kata sandi profil tersembunyi + Mikrofon + Privasi didefinisikan ulang + Hal yang mempengaruhi baterai + Saat aplikasi sedang berjalan + Notifikasi pribadi + Instan + Ubah kode sandi hapus otomatis + Kode sandi tidak diubah! + Hapus otomatis + Aktifkan kode sandi hapus otomatis + Ubah mode hapus otomatis + Kode sandi hapus otomatis diaktifkan! + Matikan tanda terima? + Aktifkan tanda terima? + Sedang + Buram media + Kuat + ANDA + Lunak + BASIS DATA OBROLAN + Setel frasa sandi untuk diekspor + Buka folder basis data + menghapus anda + Admin dapat memblokir anggota untuk semua. + Akan diaktifkan dalam obrolan pribadi! + Grup aman + Kode sesi + Verifikasi koneksi + Batas waktu tercapai saat menghubungkan ke ponsel <b>%s</b> + Ini adalah alamat SimpleX Anda! + Permintaan koneksi berulang? + Gabung ke grup Anda? + Statistik + Total + ditandai dihapus + diblokir oleh admin + %d pesan diblokir + %d pesan diblokir oleh admin + diblokir + LIVE + dimoderasi + obrolan tidak valid + diteruskan + disimpan dari %s + terima berkas belum didukung + anda + format pesan tak diketahui + format pesan tidak valid + Obrolan ini dilindungi oleh enkripsi end-to-end quantum resistant. + Pesan, berkas dan panggilan dilindungi oleh <b>enkripsi quantum resistant e2e</b> dengan perfect forward secrecy, penolakan dan pemulihan pembobolan. + Obrolan ini dilindungi oleh enkripsi end-to-end. + Catatan pribadi + koneksi %1$d + koneksi terjalin + menghubungkan… + Anda bagikan tautan sekali + Anda bagikan tautan sekali samaran + via tautan grup + samaran via tautan grup + via tautan alamat kontak + samaran via tautan alamat kontak + via tautan sekali + Alamat kontak SimpleX + samaran via tautan sekali + Tautan lengkap + Tautan grup SimpleX + Tautan SimpleX + Undangan sekali SimpleX + via %1$s + Gagal simpan server XFTP + Nama tampilan tidak valid! + Gagal membuat profil! + Kesalahan koneksi + Gagal mengirim pesan + Gagal membuat pesan + Waktu koneksi habis + pengiriman tidak sah + Teks ini tersedia di pengaturan + Ketuk untuk memulai obrolan baru + Anda diundang ke grup + gabung sebagai %s + menghubungkan… + Tidak dapat kirim pesan + Bagikan pesan… + Teruskan pesan… + Bagikan media… + Bagikan berkas… + Kesalahan decoding + Video terkirim + Diminta untuk menerima video + Menunggu berkas + Perubahan alamat akan dibatalkan. Alamat penerima lama akan digunakan. + Kembali + Lebih lanjut + Atau pindai kode QR + Simpan + Simpan undangan tidak terpakai? + Anda dapat melihat tautan undangan lagi dalam detail koneksi. + Alamat SimpleX + Atau perlihatkan kode ini + Pindai kode keamanan dari aplikasi kontak Anda. + Kode keamanan + Kode keamanan salah! + Cara menggunakannya + Frasa sandi & ekspor basis data + Buat profil obrolan + Markdown dalam pesan + Simpan server + Tambah server + Tambah server prasetel + Uji server gagal! + Server uji + Server uji + Masukkan server manual + Server Prasetel + Pindai kode QR server + Beberapa server gagal dalam pengujian: + Alamat server Anda + Hapus server + Server WebRTC ICE yang disimpan akan dihapus. + Server ICE Anda + Cara menggunakan server Anda + Simpan + Pengaturan jaringan lainnya + Gagal simpan server ICE + Pastikan alamat server WebRTC ICE dalam format yang benar, dipisahkan baris dan tidak terduplikasi. + Gunakan proxy SOCKS + Pengaturan proxy SOCKS + memulai… + Privasi Anda + Cadangan data aplikasi + %s dan %s + Sistem + Balasan diterima + Skala + Hanya kontak yang dapat kirim pesan suara. + Nama berkas pribadi + Pesan yang lebih baik + Menyimpan %1$s pesan + %1$d berkas gagal diunduh. + Gabung + Batal pratinjau berkas + Berkas + Pesan suara tidak diizinkan + Hapus alamat? + Keluar dari grup? + Rangkaian ini bukan tautan koneksi! + Tempel tautan yang Anda terima + Bagikan dengan kontak + Ya + Kustomisasi tema + Anda bergabung dengan grup melalui tautan ini. + Mendukung bluetooth dan peningkatan lainnya. + Pengarsipan basis data + Protokol SimpleX ditinjau oleh Trail of Bits. + - pesan suara hingga 5 menit.\n- kustom waktu pesan sementara.\n- riwayat edit. + Diaktifkan untuk + %dh + Alamat server tidak valid! + Server XFTP lainnya + Gunakan kredensial proksi yang berbeda untuk setiap profil. + Server penerusan %1$s gagal terhubung ke server tujuan %2$s. Coba lagi nanti. + Cari atau tempel tautan SimpleX + Preferensi obrolan yang dipilih melarang pesan ini. + Lampiran + Ikon konteks + Batal pratinjau gambar + panggilan ditolak + panggilan video (tidak dienkripsi e2e) + Negosiasi ulang enkripsi gagal. + gandakan pesan + hash pesan buruk + Mulai obrolan? + Jelajah dan gabung ke grup + Perutean pesan pribadi 🚀 + Lindungi alamat IP Anda dari relai pesan yang dipilih oleh kontak Anda.\nAktifkan di pengaturan *Jaringan & server*. + Tempel tautan + Bagikan tautan undangan 1-kali + Tempel + Coba lagi + Hubungkan melalui tautan + Teks yang Anda tempel bukan tautan SimpleX. + Pengaturan Anda + Alamat SimpleX Anda + Profil obrolan Anda + Server SMP dikonfigurasi + Server SMP lainnya + Gunakan server + Server XFTP dikonfigurasi + Periksa alamat server dan coba lagi. + Tampilkan persentase + Kontribusi + Instal SimpleX Chat untuk terminal + Panggilan video masuk + mengundang %1$s + pemilik + Kode sandi hapus otomatis + Temukan obrolan lebih cepat + Statistik terperinci + Berkas besar! + Pindai / Tempel tautan + Berikan izin untuk melakukan panggilan + panggilan suara (tidak dienkripsi e2e) + Tema kustom + - terhubung ke layanan direktori (BETA)!\n- tanda terima pengiriman (hingga 20 anggota).\n- lebih cepat dan stabil. + Mode samaran sederhana + Terapkan + Hubung ulang + Kesalahan pengenalan + Keluar + enkripsi disetujui + Anda dan kontak Anda dapat mengirim pesan sementara. + Hanya kontak Anda yang dapat mengirim pesan sementara. + Anda dan kontak Anda dapat mengirim pesan suara. + Kode sandi aplikasi + Atur sebagai ganti autentikasi sistem. + Akhirnya, kita mendapatkannya! 🚀 + Antarmuka Polandia + Membuat satu pesan dihapus + Bahkan saat dimatikan dalam percakapan. + Centang kedua yang terlewat! ✅ + Filter obrolan belum dibaca dan favorit. + Buat profil baru di aplikasi desktop. 💻 + - opsional memberi tahu kontak yang dihapus.\n- nama profil dengan spasi.\n- dan masih banyak lagi! + Buramkan untuk privasi lebih baik. + Perangkat ini + Berkas + Kabel ethernet + Hapus pesan tidak bisa dibatalkan dilarang. + Kirim berkas dan media dilarang. + Aplikasi mengenkripsi berkas lokal baru (kecuali video). + Aktifkan mode samaran saat menghubungkan. + Peningkatan pengiriman pesan + Penggunaan baterai yang sedikit. + UI Hongaria dan Turki + Enkripsi quantum resistant + Periksa pembaruan + Silakan coba lagi nanti. + Kesalahan perutean pribadi + Tambah alamat ke profil Anda, sehingga kontak dapat membagikannya dengan orang lain. Pembaruan profil akan dikirim ke kontak Anda. + Android Keystore digunakan untuk simpan frasa sandi dengan aman setelah Anda memulai ulang aplikasi atau ubah frasa sandi - ini mungkin dapat menerima notifikasi. + Aktifkan panggilan dari layar kunci melalui Pengaturan. + 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. + Hapus gambar + Ukuran huruf + Kirim pesan pribadi ke anggota dilarang. + Kirim tautan SimpleX dilarang + Reaksi pesan dilarang. + Tautan SimpleX dilarang. + Server + Tak terlindungi + Sesi aplikasi + Kredensial SOCKS baru akan digunakan untuk setiap server. + Build aplikasi: %s + Versi inti: v%s + Ketika IP disembunyikan + WARNA ANTARMUKA + Fallback perutean pesan + Mode routing pesan + Routing pribadi + JANGAN gunakan routing pribadi. + Gunakan routing pribadi dengan server yang tak dikenal ketika alamat IP tidak dilindungi. + Stable + Pembaruan tersedia: %s + Dimatikan + Lewati versi ini + Pembaruan aplikasi diunduh + Buat alamat + ID Basis Data dan Opsi Isolasi Transport. + panggilan tak terjawab + Anda dapat membuatnya terlihat oleh kontak SimpleX Anda melalui Pengaturan. + Undang + Hai!\nHubungi saya melalui SimpleX Chat: %s + Konfirmasi kata sandi + panggilan gagal + menunggu jawaban… + menerima jawaban… + menerima konfirmasi… + Pindah dari perangkat lain + Buka pengaturan + Kamera dan mikrofon + Panggilan pada layar terkunci: + Speaker + Izin dalam pengaturan + Perpesanan masa depan + Temukan izin ini di pengaturan Android dan ubah secara manual. + Earpiece + Matikan + Server ICE Anda + Server ICE WebRTC + Jika Anda memasukkan kode sandi hapus otomatis saat membuka aplikasi: + IKON APLIKASI + Aplikasi akan meminta untuk mengonfirmasi unduhan dari server berkas yang tidak dikenal (kecuali .onion atau saat proxy SOCKS diaktifkan). + Reaksi pesan dilarang dalam obrolan ini. + Pindah ke perangkat lain + Untuk melakukan panggilan, izinkan penggunaan mikrofon. Akhiri panggilan dan coba panggil lagi. + Klik tombol info di dekat kolom alamat untuk mengizinkan penggunaan mikrofon. + Server relai melindungi alamat IP Anda, tetapi dapat mengamati durasi panggilan. + Buka Pengaturan Safari / Situs Web / Mikrofon, lalu pilih Izinkan untuk localhost. + Buka SimpleX Chat untuk terima panggilan + Buka + terenkripsi e2e + peer-to-peer + kontak tidak memiliki enkripsi e2e + via relai + Panggilan tak terjawab + Panggilan ditolak + ID pesan berikutnya salah (kurang atau sama dengan yang sebelumnya).\nHal ini dapat terjadi karena beberapa bug atau ketika koneksi terganggu. + TEMA + KIRIM TANDA TERIMA KIRIMAN KE + Hal ini dapat terjadi ketika Anda atau koneksi Anda menggunakan cadangan basis data lama. + Android Keystore digunakan untuk menyimpan frasa sandi dengan aman - memungkinkan layanan notifikasi berfungsi. + Hapus + Enkripsi + Pesan + pembuat + diundang melalui tautan grup Anda + anggota + keluar + Gelap + Gelap + Sistem + Menu & peringatan + Judul + Latar wallpaper + Aksen wallpaper + Mode sistem + Selamat siang! + Selamat pagi! + Ulangi + Isi + ya + Preferensi grup + Reaksi pesan + diaktifkan untuk kontak + Anda dan kontak dapat melakukan panggilan. + Kirim hingga 100 pesan terakhir untuk anggota baru. + Kirim pesan sementara dilarang. + Jangan perlihat pesan riwayat ke anggota baru. + Anggota dapat mengirim pesan pribadi. + Pesan suara dilarang. + Anggota dapat memberi reaksi pesan. + %d bulan + pemilik + %d dtk + %dd + %d mnt + %d bulan + %db + %dbln + Apa yang baru + Anggota dapat kirim berkas dan media. + Berkas dan media dilarang. + Riwayat pesan tidak dikirim ke anggota baru. + Sembunyikan layar aplikasi di aplikasi terbaru. + Kontak Anda dapat mengizinkan hapus semua pesan. + Pesan terkirim akan dihapus setelah waktu yang ditentukan. + Verifikasi keamanan koneksi + Nama, avatar, dan isolasi transport yang berbeda. + Draf pesan + Lindungi profil obrolan Anda dengan kata sandi! + Terima kasih kepada pengguna – berkontribusi melalui Weblate! + Cepat dan tidak perlu menunggu pengirim online! + UI Jepang dan Portugis + Catatan pribadi + Enkripsi berkas & media tersimpan + Dengan berkas dan media terenkripsi. + Hubungkan aplikasi ponsel dan desktop! 🔗 + Tanda terima kirim pesan! + Buat grup menggunakan profil acak. + Blokir anggota grup + Suara panggilan masuk + Saat menghubungkan panggilan suara dan video. + Tempel tautan untuk terhubung! + Riwayat terkini dan peningkatan bot direktori. + Aktifkan dalam obrolan pribadi (BETA)! + Pindah ke perangkat lain melalui kode QR. + Hapus hingga 20 pesan sekaligus. + Periksa pembaruan + Unduh %s (%s) + simplexmq: v%s (%2s) + Gunakan routing pribadi dengan server yang tak dikenal. + Panggilan lebih baik + Terhubung ke diri sendiri? + Hapus atau moderasi hingga 200 pesan. + Teruskan hingga 20 pesan sekaligus. + Segera hadir! + Ganti suara dan video selama panggilan. + Pengalaman pengguna lebih baik + Total terkirim + Pesan diterima + Mulai ulang obrolan + Otentikasi proxy + Gunakan kredensial acak + menunggu konfirmasi… + Berikan izin + Gagal membuka peramban + Peramban web bawaan diperlukan untuk panggilan. Harap konfigurasikan peramban bawaan dalam sistem, dan bagikan informasi lebih lanjut dengan pengembang. + Aktifkan untuk semua + Nonaktifkan untuk semua grup + Undang anggota + Sistem + Terang + SimpleX + Berkas dan media + Tautan SimpleX + Perlihat riwayat + Panggilan suara/video + Hapus setelah + Dengan kurangi penggunaan baterai. + Toolbar obrolan mudah diakses + UI Persia + Grup terbuka + WiFi + Mulai dari %s.\nSemua data bersifat pribadi di perangkat Anda. + Sesi transport + Total diterima + Terima galat + upaya + Dikenal + Menunggu gambar + Menunggu video + PERANGKAT + OBROLAN + BERKAS + Reset semua petunjuk + Gagal menambah anggota + Gagal gabung ke grup + Pengirim batalkan kirim berkas. + Tak bisa menerima berkas + Tanpa Tor atau VPN, alamat IP Anda akan terlihat oleh relay XFTP ini:\n%1$s. + Gagal menerima berkas + Gagal membuat alamat + Anda sudah terhubung ke %1$s. + Tautan koneksi tidak valid + Harap periksa apakah tautan yang digunakan benar atau minta kontak Anda untuk kirim tautan lain. + Gagal menerima permintaan kontak + Galat + Mungkin sidik jari sertifikat di alamat server salah + Gagal mengatur alamat + Gagal hapus profil pengguna + Hapus antrian + Buat berkas + Hapus berkas + Gagal perbarui privasi pengguna + Fungsi lambat + Notifikasi instan + <b>Izinkan</b> pada dialog berikutnya untuk menerima pemberitahuan secara instan. + Optimalisasi baterai aktif, mematikan layanan latar belakang dan permintaan pesan baru secara berkala. Anda dapat aktifkan kembali di pengaturan. + Buka pengaturan aplikasi + Notifikasi berkala dinonaktifkan! + Matikan notifikasi + Untuk lakukan panggilan di latar belakang, pilih <b>Penggunaan baterai aplikasi</b> / <b>Tidak dibatasi</b> di pengaturan aplikasi. + Frasa sandi diperlukan + Untuk menerima notifikasi, mohon masukkan frasa sandi basis data + Akhiri + Tidak dapat inisialisasi basis data + Basis data tidak berfungsi dengan benar. pelajari lebih lanjut + Tutup + Dimulai secara berkala + Berjalan saat aplikasi terbuka + Aplikasi hanya menerima notifikasi saat sedang berjalan, tidak ada layanan latar belakang yang dimulai + Memeriksa pesan baru setiap 10 menit hingga 1 menit + Tersembunyi + Tampilkan kontak dan pesan + Kontak tersembunyi: + Nyalakan + Mode Kunci SimpleX + Kunci SimpleX diaktifkan + Anda akan diminta untuk lakukan autentikasi saat mulai atau lanjutkan aplikasi setelah 30 detik di latar belakang. + Kode sandi aplikasi diganti dengan kode sandi hapus otomatis. + Mereka dapat ditimpa dalam pengaturan kontak dan grup. + Aktifkan (tetap ditimpa) + Matikan (tetap ditimpa) + Gagal memuat obrolan + Gagal hapus kontak + Gagal memuat server SMP + Gagal memuat obrolan + Nama tampilan ini tidak valid. Silakan pilih nama lain. + Pengirim mungkin telah hapus permintaan koneksi. + Server perlu otorisasi untuk membuat antrian, periksa kata sandi + Gagal menghapus permintaan kontak + Gagal menghapus koneksi kontak tertunda + Gagal mengubah alamat + Gagal batalkan perubahan alamat + Pesan SimpleX Chat + Untuk aktifkan notifikasi, pilih <b>Penggunaan baterai aplikasi</b> / <b>Tidak dibatasi</b> di pengaturan aplikasi. + Aplikasi mungkin ditutup setelah 1 menit di latar belakang. + Untuk melindungi informasi Anda, aktifkan Kunci SimpleX.\nAnda akan diminta untuk menyelesaikan autentikasi sebelum fitur ini aktif. + %d menit + Gagal menampilkan notifikasi, hubungi pengembang. + Aktifkan tanda terima untuk grup? + Profil obrolan kosong dengan nama yang disediakan dibuat, dan aplikasi terbuka seperti biasa. + Jika Anda memasukkan kode sandi saat membuka aplikasi, semua data aplikasi akan dihapus secara permanen! + Pengaturan ini untuk profil Anda saat ini + Kirim tanda terima diaktifkan untuk %d kontak + Kirim tanda terima dimatikan untuk %d kontak + Gagal memuat server XFTP + Gagal memperbarui konfigurasi jaringan + Mohon perbarui aplikasi dan hubungi pengembang. + Anda sudah memiliki nama tampilan profil obrolan yang sama. Silakan pilih nama lain. + Periksa koneksi jaringan Anda dengan %1$s dan coba lagi. + Gagal memuat detail + Gagal menghapus grup + Gagal hapus catatan pribadi + Putuskan + Amankan antrian + Unduh berkas + Bandingkan berkas + Notifikasi instan! + Notifikasi instan dimatikan! + Notifikasi berkala + SimpleX tidak dapat berjalan di latar belakang. Anda hanya menerima notifikasi saat aplikasi berjalan. + Panggilan video + Menerima pesan… + Lihat pratinjau + Teks pesan + Layanan latar belakang selalu berjalan – notifikasi selalu menampilkan pesan yang tersedia. + Tampilkan hanya kontak + Sembunyikan kontak dan pesan + Kunci SimpleX + Autentikasi sistem + Autentikasi gagal + Entri kode sandi + Anda tidak dapat diverifikasi; silakan coba lagi. + Versi server penerusan tidak kompatibel dengan pengaturan jaringan: %1$s. + Versi server tujuan %1$s tidak kompatibel dengan server penerusan %2$s. + Kesalahan koneksi (AUTH) + Gagal menghubungkan ke server penerusan %1$s. Coba lagi nanti. + Alamat server penerusan tidak kompatibel dengan pengaturan jaringan: %1$s. + Unggah berkas + Eksekusi fungsi memakan waktu terlalu lama: %1$d detik: %2$s + Panggilan SimpleX Chat + Alamat server tidak kompatibel dengan pengaturan jaringan: %1$s. + Uji gagal pada langkah %s. + %d detik + Segera + Matikan tanda terima untuk grup? + Kirim tanda terima diaktifkan untuk %d grup + Kirim tanda terima dimatikan untuk %d grup + Aktifkan (grup tetap ditimpa) + Matikan (grup tetap ditimpa) + Matikan untuk semua + Aktifkan untuk semua grup + Autentikasi + Versi server tidak kompatibel dengan aplikasi Anda: %1$s. + Server tak dikenal! + 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. + Gagal sinkronkan koneksi + Server perlu otorisasi untuk mengunggah, periksa kata sandi + Buat antrian + <b>Dapat dimatikan melalui pengaturan</b> – notifikasi akan tetap ditampilkan saat aplikasi berjalan. + Untuk tingkatkan privasi, <b>SimpleX berjalan di latar belakang</b> alih-alih gunakan notifikasi push. + 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. + Layanan SimpleX Chat + Nama kontak + Masukkan Kode Sandi + Kode Sandi Saat Ini + Ubah kode sandi + Harap diingat dan simpan dengan aman - tidak ada cara untuk pulihkan kata sandi yang hilang! + Kiriman debug + Hapus + Dibuat pada: %s + diblokir + Ganti + Buka blokir + Buka blokir anggota untuk semua? + Buka untuk semua + Diblokir oleh admin + ANGGOTA + Hapus anggota + Status pesan: %s + Status berkas: %s + Grup + dimatikan + Buka blokir anggota? + Buka blokir anggota + tidak aktif + Hapus anggota? + Hapus anggota + Pesan tersimpan + Tak dapat memanggil anggota grup + Ketentuan diterima untuk operator: <b>%s</b>. + Ketentuan akan diterima untuk operator: <b>%s</b>. + Operator + Gunakan server + Gunakan %s + Tak dapat kirim pesan ke anggota grup + Pesan sambutan + Simpan pesan sambutan? + Perbaikan tidak didukung oleh anggota grup + Buat grup rahasia + Negosiasi ulang enkripsi + Masukkan nama grup: + Pratinjau + Kirim pesan untuk aktifkan panggilan. + Kontak dihapus. + Tak dapat memanggil kontak + Perbaiki + Sepenuhnya terdesentralisasi – hanya terlihat oleh anggota. + Perbaiki koneksi + Perbaiki koneksi? + Tinjau ketentuan + Server prasetel + Ketentuan diterima + Ketentuan akan otomatis diterima untuk operator yang diaktifkan pada: %s. + Anda perlu izinkan kontak Anda agar dapat memanggilnya. + Pesan terlalu besar + Masukkan pesan sambutan… + Simpan dan perbarui profil grup + Pesan sambutan terlalu panjang + Perbaikan tidak didukung oleh kontak + Obrolan + Terima kondisi + SERVER + Buat grup + Nama lengkap grup: + Simpan profil grup + Peramban + Server Anda + Gagal simpan profil grup + %s server + Operator jaringan + Ketentuan diterima pada: %s. + Ketentuan akan diterima pada: %s. + Koneksi + Rol + Ganti rol + Profil obrolan Anda akan dikirim ke anggota grup + Profil obrolan Anda akan dikirim ke anggota obrolan + Profil grup disimpan di perangkat anggota, bukan di server. + langsung + Kirim via + Terima via + Kontak diperiksa + Tak dapat undang kontak! + enkripsi disetujui untuk %s + pembuat + menghubungkan + Hapus grup? + Grup akan dihapus untuk semua anggota - ini tidak dapat dibatalkan! + Buat tautan grup + menghubungkan (diperkenalkan) + kode keamanan berubah + %d kontak dipilih + Tautan grup + Anggota lama %1$s + enkripsi end-to-end standar + enkripsi e2e quantum resistant + menghubungkan (undangan perkenalan) + tidak dikenal + Hapus obrolan + Tambah teman + Tambah anggota tim + Keluar grup + menghubungkan (diumumkan) + menghubungkan (diterima) + Hapus grup + Grup akan dihapus untuk Anda - ini tidak dapat dibatalkan! + Hapus obrolan? + Obrolan akan dihapus untuk semua anggota - ini tidak dapat dibatalkan! + Obrolan akan dihapus untuk Anda - ini tidak dapat dibatalkan! + Tinggalkan obrolan + Edit profil grup + Pesan sambutan + Undang anggota + anda: %1$s + Anda mencoba mengundang kontak yang telah Anda bagikan profil samaran ke grup tempat Anda menggunakan profil utama + Undang ke grup + Undang ke obrolan + Pilih kontak + Lewati undang anggota + Alamat atau tautan 1-kali? + Opsi pengembang + Operator server + Operator jaringan + Aplikasi ini melindungi privasi Anda dengan gunakan operator yang berbeda setiap percakapan. + Ketentuan akan diterima untuk operator yang diaktifkan setelah 30 hari. + Alat pengembang + Warna obrolan + terhubung langsung + Koneksi ke desktop dalam kondisi buruk + Konfirmasi hapus kontak? + Waktu kustom + Pilih berkas + Anda dapat <font color="#0088ff">terhubung ke pengembang SimpleX Chat untuk mengajukan pertanyaan dan menerima pembaruan</font>. + Hapus obrolan? + Jangan gunakan kredensial dengan proxy. + Atur <i>Gunakan host .onion</i> ke Tidak jika proxy SOCKS tidak mendukung. + <b>Harap diperhatikan</b>: relay pesan dan berkas terhubung melalui proxy SOCKS. Panggilan dan pengiriman pratinjau tautan menggunakan koneksi langsung. + JANGAN mengirim pesan secara langsung, meskipun server Anda atau server tujuan tidak mendukung routing pribadi. + Nama tampilan tidak boleh terdapat spasi. + Frasa sandi acak disimpan dalam pengaturan sebagai teks biasa.\nAnda dapat mengubahnya nanti. + Pasang frasa sandi basis data + Basis data akan dienkripsi. + Peningkatan basis data + Konfirmasi peningkatan basis data + Turunkan dan buka obrolan + mengubah hak %s menjadi %s + Dihapus pada: %s + Hapus profil + Ini adalah tautan untuk grup <b>%1$s</b>! + Batal pindah + Potongan diunggah + hubungkan + Percakapan dihapus! + Pesan sementara + Hapus catatan pribadi? + Obrolan dihentikan + Frasa sandi basis data + Konfirmasi frasa sandi baru… + Ubah frasa sandi basis data? + Tidak dapat mengakses Keystore untuk menyimpan kata sandi basis data + Penurunan basis data + Obrolan dihentikan + Buat tautan + Dihapus di + Pesan pribadi antar anggota dilarang dalam obrolan ini. + Tanda terima pengirim! + <i>(perangkat ini v%s)</i> + Perangkat + Koneksi terputus + Gagal salin + Temukan melalui jaringan lokal + Kode undangan desktop salah + Desktop terputus + Anda sudah terhubung ke <b>%1$s</b>. + Konfirmasi pengaturan jaringan + Potongan dihapus + Potongan diunduh + Berkas terunduh + Gagal unduh + Hapus semua berkas + Hapus berkas dan media? + %d berkas dengan total ukuran %s + Lanjutkan + kontak dihapus + Tak dapat undang kontak! + Terhapus pada + Terhapus pada: %s + (saat ini) + Ketentuan yang sama akan berlaku untuk operator <b>%s</b>. + Buram + Transparansi + Bilah alat aplikasi + Status koneksi dan server. + Desktop memiliki versi tidak didukung. Pastikan Anda gunakan versi yang sama di kedua perangkat + Obrolan dipindahkan! + Hapus basis data dari perangkat ini + Hapus berkas untuk semua profil obrolan + Dibuat di + <b>Terbaik untuk baterai</b>. Anda akan menerima notifikasi saat aplikasi sedang berjalan (TANPA layanan latar belakang). + <b>Baik untuk baterai</b>. Aplikasi memeriksa pesan setiap 10 menit. Anda mungkin melewatkan panggilan atau pesan penting. + Tema obrolan + BASIS DATA OBROLAN + Basis data dienkripsi menggunakan frasa sandi acak. Harap ubah frasa sandi sebelum mengekspor. + Basis data obrolan diekspor + Frasa sandi saat ini… + <b>Harap diperhatikan</b>: Anda TIDAK akan dapat pulihkan atau ubah frasa sandi jika hilang. + Basis data dienkripsi menggunakan frasa sandi acak, Anda dapat mengubahnya. + Basis data akan dienkripsi dan frasa sandi disimpan dalam pengaturan. + versi database lebih baru daripada aplikasi, tetapi tidak ada penurunan migrasi untuk: %s + %d acara grup + mengubah alamat untuk Anda + ID basis data + Menghubungkan ke kontak, harap tunggu atau periksa nanti! + Hapus profil obrolan? + Gagal hapus + <b>Perangkat Xiaomi</b>: harap aktifkan Autostart di pengaturan sistem agar notifikasi berfungsi. + Hapus obrolan + Hubungkan via tautan / kode QR + Profil acak baru akan dibagikan. + Permintaan koneksi terkirim! + Keamanan koneksi + Nama telah benar untuk %s? + Anda dapat konfigurasi operator di pengaturan Jaringan dan server. + Kesalahan basis data + Frasa sandi basis data berbeda dengan yang disimpan di Keystore. + Ubah hak grup? + Ketentuan ini juga berlaku untuk: <b>%s</b>. + Ketentuan Penggunaan + Ketentuan akan diterima untuk operator: <b>%s</b>. + Untuk gunakan server <b>%s</b>, terima ketentuan penggunaan. + Menambahkan server pesan + Hapus profil obrolan + Obrolan bisnis + Terputus dikarenakan: %s + Terputus dari ponsel <b>%s</b> dengan alasan: %s + Desktop sedang sibuk + grup dihapus + kontak %1$s diubah menjadi %2$s + Panggilan dilarang! + Ketentuan sudah diterima untuk operator berikut: <b>%s</b>. + Ketentuan yang sama akan berlaku untuk operator: <b>%s</b>. + Teks ketentuan saat ini tidak dapat dimuat, Anda dapat meninjau ketentuan via tautan ini: + Pesan pribadi antar anggota dilarang. + Pilih <i>Pindah dari perangkat lain</i> pada perangkat baru dan pindai kode QR. + Kontak dan semua pesan akan dihapus - ini tidak dapat dibatalkan! + Kontak akan dihapus - ini tidak dapat dibatalkan! + Hapus kontak? + Kontak dihapus! + Hapus tanpa notifikasi + Ubah alamat penerima? + Terputus + <b>Tambahkan kontak</b>: untuk buat tautan undangan baru, atau terhubung via tautan yang Anda terima. + Hapus koneksi yang tertunda? + Kontak belum terhubung! + Hapus alamat + Hapus basis data + Basis data obrolan dihapus + Basis data obrolan diimpor + Dimatikan + Kontrol jaringan Anda + Menghubungkan ke desktop + Versi aplikasi desktop %s tidak kompatibel dengan aplikasi ini. + Koneksi ponsel <b>%s</b> dalam kondisi buruk + Autentikasi perangkat tidak diaktifkan. Anda dapat aktifkan SimpleX Lock via Pengaturan, setelah mengaktifkan autentikasi perangkat. + Autentikasi tidak tersedia + Konfirmasi kredensial Anda + Autentikasi perangkat mati. Matikan SimpleX Lock. + Matikan Kunci SimpleX + Kesalahan server tujuan: %1$s + Hapus dan beritahu kontak + Hapus kontak + Batal pesan langsung + Konfirmasi + Buat grup rahasia + Kamera tidak tersedia + 💻 desktop: pindai kode QR yang ditampilkan dari aplikasi, via <b>Pindai kode QR</b>. + 📱 ponsel: ketuk <b>Buka di aplikasi seluler</b>, lalu ketuk <b>Hubungkan</b> di aplikasi. + Hapus + Hapus + batal pratinjau tautan + Tombol tutup + Email + Jika Anda tidak dapat bertemu langsung, Anda dapat <b>pindai kode QR dalam panggilan video</b>, atau kontak Anda dapat bagikan tautan undangan. + Lebih lanjut di <font color="#0088ff">Panduan Pengguna</font>. + Aktifkan + Ketika lebih dari satu operator diaktifkan, tidak satupun dari mereka memiliki metadata untuk mengetahui siapa yang berkomunikasi. + Obrolan sedang berjalan + Basis data akan dienkripsi dan frasa sandi disimpan di Keystore. + mengubah hak Anda jadi %s + mengubah alamat… + mengubah alamat untuk %s… + ID basis data: %d + Menambahkan media dan berkas server + Obrolan sudah ada! + Basis data dienkripsi! + Frasa sandi enkripsi basis data akan diperbarui dan disimpan di pengaturan. + Frasa sandi enkripsi basis data akan diperbarui dan disimpan di Keystore. + migrasi berbeda di aplikasi/basis data: %s / %s + Jika Anda tidak dapat bertemu langsung, <b>tunjukkan kode QR dalam panggilan video</b>, atau bagikan tautan. + Jangan buat alamat + Rincian + Koneksi + Terhubung ke ponsel + Jangan aktifkan + Buka <i>Gunakan dari desktop</i> di aplikasi seluler dan pindai kode QR. + Desktop + undangan diterima + diminta untuk terhubung + Ubah profil obrolan + Kapasitas terlampaui - penerima tidak menerima pesan yang dikirim sebelumnya. + tautan 1-kali dapat digunakan <i>hanya dengan satu kontak</i> - bagikan secara langsung atau melalui messenger apa pun. + Disalin ke papan klip + Buat tautan undangan satu-kali + Saat ini maksimal ukuran berkas adalah %1$s. + Anda juga dapat terhubung dengan ketuk tautan. Jika tautan terbuka di peramban, ketuk <b>Buka di ponsel</b>. + Pengaturan alamat + Tambahkan anggota tim Anda ke percakapan. + Alamat bisnis + Semua pesan dan berkas dikirim <b>dengan enkripsi end-to-end</b>, dengan keamanan post-quantum dalam pesan pribadi. + Buat tautan 1-kali + Notifikasi dan baterai + Tiada layanan latar belakang + Aplikasi selalu berjalan di latar belakang + Periksa pesan setiap 10 menit + Pilih operator jaringan yang akan digunakan. + Misalnya, jika kontak Anda menerima pesan melalui server SimpleX Chat, aplikasi Anda akan mengirimkannya melalui server Flux. + Bagaimana ini membantu privasi + Lanjutkan + Anda dapat konfigurasi server di pengaturan. + Lebih lengkap di <font color="#0088ff">repositori GitHub</font> kami. + Nilai nanti + Perbarui + Rincian tautan unduhan + Koneksi TCP terpisah (dan kredensial SOCKS) akan digunakan <b>untuk setiap profil obrolan yang Anda miliki di aplikasi</b>. + 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. + Mengunduh pembaruan aplikasi, jangan tutup aplikasi + Mati + Buat alamat SimpleX + Buat alamat agar orang dapat terhubung dengan Anda. + Lanjutkan + Hapus gambar + Edit gambar + <b>Mengonsumsi banyak baterai</b>! Aplikasi selalu berjalan di latar belakang – notifikasi ditampilkan secara instan. + Sudut + Jangan tampilkan lagi + Konfirmasi unggahan + Arsip dan unggah + Dihapus + Koneksi dihentikan + Alamat desktop + Perangkat desktop + Tentang operator + SimpleX Chat dan Flux membuat kesepakatan untuk sertakan server yang dioperasikan Flux ke aplikasi. + Hapus profil obrolan? + Obrolan dihentikan. Jika Anda sudah gunakan basis data ini di perangkat lain, Anda harus transfer kembali sebelum memulai obrolan. + Frasa sandi enkripsi basis data akan diperbarui. + Frasa sandi basis data diperlukan untuk membuka obrolan. + Ketentuan ini juga berlaku untuk: <b>%s</b>. + Anda <b>tidak</b> boleh menggunakan basis data yang sama pada dua perangkat. + Terhubung ke desktop + Hubungkan ke desktop + hari + kesalahan dekripsi + Putuskan ponsel + Dapat ditemukan melalui jaringan lokal + duplikat + Keamanan yang lebih baik ✅ + Bentuk pesan yang dapat disesuaikan. + <b>Harap diperhatikan</b>: menggunakan database yang sama pada dua perangkat akan merusak dekripsi pesan dari koneksi Anda, sebagai perlindungan keamanan. + Anda sudah bergabung dengan grup <b>%1$s</b>. + mengubah alamat… + Hapus tautan? + Hapus tautan + Putuskan + Server terhubung + Hapus pesan anggota? + Tanggal pesan lebih baik. + <i>(baru)</i> + Anda sudah terhubung dengan <b>%1$s</b>. + Dibuat + Hapus %d pesan anggota? + Gagal simpan server + Galat: %1$s + Gambar akan diterima setelah kontak Anda selesai mengunggah. + Gagal simpan berkas + Tautan tidak valid! + Instal pembaruan + Sembunyikan: + Untuk media sosial + Fitur eksperimental + Aktifkan Kunci SimpleX + Berkas tidak ditemukan - kemungkinan besar telah dihapus atau dibatalkan. + Kesalahan berkas server: %1$s + Balasan untuk + Berkas + gambar + Favorit + Keluar tanpa menyimpan + Berkas: %s + undangan ke grup %1$s + Grup tidak ditemukan! + Gagal kirim undangan + Undang + Status berkas + Gagal ganti hak + Gagal hapus anggota + Samaran + Mode samaran melindungi privasi Anda dengan menggunakan profil acak baru untuk setiap kontak. + Gagal aktifkan tanda terima pengirim! + Masukkan nama perangkat ini… + Impor gagal + Gagal unduh arsipan + Berkas yang diekspor tidak ada + Selesaikan migrasi + Gagal atur ulang statistik + Gagal impor basis data obrolan + Gagal hapus basis data obrolan + Berkas dan media + Enkripsi basis data? + Versi basis data tidak kompatibel + UNTUK KONSOL + Grup sudah ada! + Masukkan frasa sandi + Aktifkan hapus pesan otomatis? + Basis data terenkripsi + Konfirmasi migrasi tidak valid + Undangan grup kedaluwarsa + negosiasi ulang enkripsi diperbolehkan + negosiasi ulang enkripsi diperlukan + negosiasi ulang enkripsi diizinkan untuk %s + Aktifkan Flux di pengaturan Jaringan dan server untuk privasi metadata yang lebih baik. + untuk privasi metadata lebih baik. + Navigasi obrolan ditingkatkan + Galat + Selesaikan migrasi pada perangkat lain. + Gagal verifikasi frasa sandi: + Gagal hubungkan ulang server + Gagal hubungkan ulang server + EKSPERIMENTAL + Ekspor basis data + Impor basis data + Gagal hentikan obrolan + Gagal ganti pengaturan + Galat: %s + Grup tidak aktif + Undangan kedaluwarsa! + profil grup diperbarui + Hak awal + Gagal membuat tautan grup + Gagal hapus tautan grup + Gagal perbarui tautan grup + Gagal membuat kontak anggota + Sembunyikan + Gambar akan diterima saat kontak Anda online, harap tunggu atau periksa nanti! + Kesalahan berkas + Dari Galeri + Cara menggunakan markdown + Undangan grup tidak lagi berlaku, telah dihapus oleh pengirim. + Untuk routing pribadi + Gagal perbarui server + Masukkan kata sandi dalam pencarian + Masukkan frasa sandi yang benar. + jam + Versi tidak kompatibel + Galat + Server penerusan: %1$s\nKesalahan: %2$s + Server penerusan: %1$s\nKesalahan server tujuan: %2$s + Berkas dan media dilarang! + Berkas akan diterima setelah kontak Anda selesai mengunggah. + Berkas akan diterima saat kontak Anda online, harap tunggu atau periksa nanti! + Aktifkan akses kamera + Jika Anda menerima tautan undangan SimpleX Chat, Anda dapat buka di peramban Anda: + bantuan + Jika Anda tidak dapat bertemu langsung, tunjukkan kode QR dalam panggilan video, atau bagikan tautan. + Aktifkan TCP keep-alive + Gagal memulai obrolan + Mode samaran + Gagal hapus basis data + Kode QR tidak valid + Gagal enkripsi basis data + Galat + Impor + kedaluwarsa + Jika Anda memilih menolak, pengirim TIDAK akan diberitahu. + Kesalahan dalam konfigurasi server. + Untuk profil obrolan %s: + Gagal menerima ketentuan + Berhasil diinstal + Sembunyikan profil + Masukkan frasa sandi… + Berkas telah dihapus atau tautan tidak valid + Masukkan pesan sambutan… (opsional) + Nama lengkap: + Untuk melanjutkan, obrolan harus dihentikan. + negosiasi ulang enkripsi diperlukan untuk %s + Perluas pemilihan hak + Ditemukan desktop + Gagal simpan pengaturan + enkripsi ok untuk %s + Gagal ekspor basis data obrolan + Impor basis data obrolan? + tidak langsung (%1$s) + Gagal menambah server + Gagal perbarui arsipan + Gagal blokir anggota untuk semua + Gagal simpan kata sandi pengguna + Tautan tidak valid + Tiada server pesan. + Tiada server untuk routing pesan pribadi. + Tiada server media dan berkas. + Tiada server untuk menerima pesan. + Atau bagikan secara pribadi + Simpan dan beritahu anggota grup + %s (saat ini) + Pesan dari %s akan ditampilkan! + Masuk dengan kredensial Anda + Kirim + Simpan dan beritahu kontak + ID pesan salah + Kesalahan Keychain + Simpan frasa sandi dan buka obrolan + Buka obrolan + 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. + Toolbar aplikasi yang dijangkau + Anggota akan dihapus dari grup - ini tidak dapat dibatalkan! + Batas waktu protokol per KB + Menerima konkurensi + Kata sandi profil + Antarmuka Italia + Hubungkan ponsel + Anda dapat aktifkan nanti di pengaturan Privasi dan Keamanan aplikasi. + Opsi desktop tertaut + Anda dapat mencoba lagi. + Reset + Mulai ulang + Mulai ulang aplikasi untuk gunakan basis data obrolan yang diimpor. + Pulihkan cadangan basis data + Gagal pulihkan basis data + Anda dapat bagikan tautan atau kode QR - siapa pun dapat bergabung ke grup. Anda tidak akan kehilangan anggota grup jika nanti Anda hapus. + Tanda terima dimatikan + Anda dapat bagikan alamat ini dengan kontak agar dapat terhubung dengan %s. + Anda dapat aktifkan nanti di Pengaturan + Kirim tanda terima pengirim akan diaktifkan untuk semua kontak di semua profil obrolan yang terlihat. + Atau tempel tautan arsip + Mempersiapkan unduhan + Pindah ke sini + Impor ulang + Harap konfirmasi bahwa pengaturan jaringan sudah benar untuk perangkat ini. + Mempersiapkan unggahan + Hubungkan ulang semua server yang terhubung untuk paksa kirim pesan. Ini menggunakan lalu lintas tambahan. + Hubungkan ulang server? + Reset semua statistik + Reset seluruh statistik? + Hubungkan ulang semua server + Simpan frasa sandi di Keystore + Nama lokal + Dikirim pada + Diterima pada + Reset ke bawaan + Jaringan desentralisasi + Kirim tanda terima pengirim akan diaktifkan untuk semua kontak. + Desktop terhubung + %s diunduh + Pesan diterima + Catatan diperbarui pada + PESAN DAN BERKAS + Tema profil + Gambar profil + Harap masukkan frasa sandi saat ini yang benar. + Migrasi: %s + %s terhubung + anggota %1$s berubah menjadi %2$s + Status pesan + Dimoderasi pada + Kirim pesan pribadi + Batas waktu protokol + Simpan dan hubungkan ulang + Memungkinkan adanya banyak koneksi anonim tanpa ada data bersama di antara mereka dalam satu profil obrolan. + Hubungkan ulang server? + Pesan terlalu besar! + Mohon kurangi ukuran pesan atau hapus media dan kirim lagi. + Mohon kurangi ukuran pesan dan kirim lagi. + Anda dapat salin dan kurangi ukuran pesan untuk kirim. + Reset + Kirim pesan langsung - pesan akan diperbarui pada penerima saat Anda mengetik + Kirim Pesan + Tandai belum dibaca + Anda dapat setel nama koneksi, untuk mengingat dengan siapa tautan dibagikan. + Buka lokasi berkas + Silakan mulai ulang aplikasi. + Gabung ke samaran + %s pada %s + Anggota akan dihapus dari obrolan - ini tidak dapat dibatalkan! + Server operator + Server ditambah ke operator %s. + Operator server berubah. + Protokol server berubah. + Jumlah PING + Interval PING + - Buka obrolan pada pesan belum dibaca pertama.\n- Lompat ke pesan yang dikutip. + Pilih + Hubungkan ulang server untuk paksa kirim pesan. Ini menggunakan lalu lintas tambahan. + Bilah alat obrolan yang dijangkau + foto profil dihapus + %s dan %s terhubung + Privasi untuk pelanggan Anda. + Unduh ulang + Anda dapat mencoba lagi. + Hapus arsip? + Unggah ulang + Simpan percakapan + Anda dapat kirim pesan ke %1$s dari kontak yang diarsip. + Alamat penerima akan diubah ke server lain. Perubahan alamat akan selesai setelah pengirim online. + Minta kontak Anda untuk aktifkan kirim pesan suara. + gambar profil + Simpan pengaturan terima otomatis + Catatan diperbarui pada: %s + dtk + Alamat desktop salah + Pindai kode QR dari desktop + Pesan terkirim + dimoderasi oleh %s + Buka konsol obrolan + Peringatan kirim pesan + Kesalahan kirim pesan + Penerima tidak dapat melihat siapa pengirim pesan ini. + Silakan hubungi admin grup. + Memuat berkas + Harap tunggu sementara berkas sedang dimuat dari ponsel yang terhubung + Tertunda + Negosiasi ulang enkripsi? + Negosiasi ulang + Kirim pesan sementara + Kirim + Pindai kode QR + (pindai atau tempel dari papan klip) + Izin Ditolak! + Tandai dibaca + tautan pratinjau gambar + Anda dapat bagikan alamat sebagai tautan atau kode QR - siapa pun dapat terhubung dengan Anda. + Simpan + Jadikan profil pribadi! + Ponsel jarak jauh + JALANKAN OBROLAN + Harap simpan frasa sandi dengan aman, Anda TIDAK akan dapat mengakses obrolan jika hilang. + dihapus %1$s + Dikirim pada: %s + ID pesan salah + Hapus frasa sandi dari Keystore? + Hapus frasa sandi dari pengaturan? + Simpan frasa sandi di pengaturan + Diproxy + Tempel tautan yang Anda terima untuk terhubung dengan kontak… + lainnya + Kirim pesan langsung + Pesan langsung! + Kode QR + Memindah + Alamat server + Buka pengaturan server + alamat kontak dihapus + Putar dari daftar obrolan. + Ponsel terhubung + Pindai dari ponsel + Lainnya + Tiada server untuk menerima berkas. + Tiada server untuk kirim berkas. + Tiada pesan + Buka layar migrasi + Pesan akan dihapus - ini tidak dapat dibatalkan! + tempat penampung gambar profil + Alamat server tidak kompatibel dengan pengaturan jaringan. + Kata sandi ditampilkan + Ingatkan nanti + Harap masukkan kata sandi sebelumnya setelah memulihkan cadangan basis data. Tindakan ini tidak dapat dibatalkan. + Pulihkan + Anda dapat mulai obrolan via Pengaturan aplikasi / Basis Data atau dengan mulai ulang aplikasi. + Hanya pemilik obrolan yang dapat ubah preferensi. + Kirim tanda terima + detik + Harap laporkan ke pengembang:\n%s\n\nDisarankan untuk memulai ulang aplikasi. + menit + Kredensial SOCKS baru akan digunakan setiap kali Anda memulai aplikasi. + Mari bicara di SimpleX Chat + Simpan dan beritahu kontak + Simpan preferensi? + Anda dapat buat nanti + Gabung ke grup? + Bergabung dengan grup + Minta kontak Anda untuk aktifkan panggilan. + Anda dapat sembunyikan atau matikan profil pengguna - tekan tahan untuk buka menu. + Penggunaan baterai sedikit + Berhasil dipindah + Rekam pesan suara + Penerima pesan + Hash pesan salah + Keluar dari obrolan? + Harap simpan frasa sandi dengan aman, Anda TIDAK akan dapat mengubahnya jika hilang. + Pulihkan cadangan basis data? + Dimoderasi pada: %s + Diterima pada: %s + Atau impor berkas arsip + Harap laporkan ke pengembang:\n%s + Nama profil: + Tertunda + Server yang terhubung sebelumnya + Server proxy + Pesan akan ditandai sebagai dihapus. Penerima tetap dapat melihat pesan tersebut. + Pesan akan ditandai sebagai dihapus. Penerima tetap dapat melihat pesan ini. + Perbaiki + Perbaiki koneksi? + Server baru + Info antrian pesan + Bentuk pesan + ROUTING PESAN PRIBADI + info antrean server: %1$s\n\npesan terakhir diterima: %2$s + Hanya data profil lokal + Buka perubahan + Ketentuan terbuka + Atau bagikan tautan berkas dengan aman + Tempel alamat desktop + Profil dan koneksi server + Kesalahan lainnya + Aman + Kesalahan kirim + Dikirim langsung + Dikirim via proxy + Pembaruan profil akan dikirim ke kontak Anda. + Tempel tautan arsip + Harap periksa apakah perangkat seluler dan desktop terhubung ke jaringan lokal yang sama, dan firewall desktop mengizinkan koneksi.\nHarap sampaikan masalah lain kepada pengembang. + Koneksi memerlukan negosiasi ulang enkripsi. + Negosiasi ulang enkripsi sedang berlangsung. + Pesan yang tidak terkirim + Pesan ini telah dihapus atau belum diterima. + Untuk terhubung via tautan + Untuk mendapatkan pemberitahuan tentang rilis baru, aktifkan pemeriksaan berkala untuk versi Stabil atau Beta. + Alamat SimpleX atau tautan 1-kali? + Matikan + Lihat konsol di jendela baru + Bunyikan + Putuskan + Buka + Kunci salah atau alamat potongan berkas tidak dikenal - kemungkinan berkas dihapus. + Versi server tidak kompatibel dengan pengaturan jaringan. + Anda adalah pengamat + Untuk memulai obrolan baru + Video + Koneksi yang Anda terima akan dibatalkan! + Kode QR ini bukan tautan! + Bunyikan + Isolasi transport + Perbarui mode isolasi transport? + Untuk melindungi alamat IP, routing pribadi menggunakan server SMP untuk mengirim pesan. + Perlihat kesalahan internal + Lihat panggilan API lambat + Kami tidak menyimpan kontak atau pesan Anda (setelah terkirim) di server. + Profil, kontak, dan pesan terkirim Anda disimpan di perangkat Anda. + Platform perpesanan dan aplikasi yang melindungi privasi dan keamanan Anda. + Untuk melindungi privasi Anda, SimpleX gunakan ID terpisah untuk setiap kontak. + PROXY SOCKS + Alihkan daftar obrolan: + Tingkatkan dan buka obrolan + Ketuk untuk gabung ke samaran + Anda memblokir %s + %s, %s dan %d anggota lainnya terhubung + Grup ini memiliki lebih dari %1$d anggota, tanda terima kiriman tidak dikirimkan. + Permintaan koneksi akan dikirim ke anggota grup ini. + Perlihat profil obrolan + Profil random Anda + %s diunggah + Aktifkan log + Profil hanya dibagikan dengan kontak Anda. + Ekor + Berhenti + Tindakan ini tidak dapat dibatalkan - profil, kontak, pesan, dan berkas Anda akan hilang secara permanen. + Beberapa kesalahan tidak fatal terjadi selama impor: + Anda dapat menyimpan arsip yang diekspor. + Terima kasih kepada pengguna – kontribusi via Weblate! + Ini adalah tautan 1-kali milik Anda! + Pesan akan ditandai sebagai dimoderasi untuk semua anggota. + Pesan akan dihapus untuk semua anggota. + Pesan akan ditandai sebagai dimoderasi untuk semua anggota. + Video akan diterima saat kontak Anda selesai mengunggah. + Kontak Anda mengirim berkas yang lebih besar dari ukuran maksimal (%1$s). + Kesalahan berkas sementara + Pesan suara… + Anda menerima koneksi + Anda harus menggunakan versi terbaru basis data obrolan Anda pada satu perangkat SAJA, jika tidak, Anda mungkin berhenti menerima pesan dari beberapa kontak. + Beberapa berkas tidak diekspor + Anda dapat memindahkan basis data yang diekspor. + Frasa sandi akan disimpan dalam pengaturan sebagai teks biasa setelah Anda mengubahnya atau memulai ulang aplikasi. + Upaya untuk mengubah frasa sandi basis data tidak selesai. + Waktu koneksi TCP habis + Terima kasih kepada pengguna – kontribusi via Weblate! + Arsip basis data yang diunggah akan dihapus secara permanen dari server. + Peringatan: memulai obrolan di beberapa perangkat tidak didukung dan akan menyebabkan gagal kirim pesan + Verifikasi frasa sandi + Kesalahan unggah + Setel frasa sandi + Tindakan ini tidak dapat dibatalkan - pesan yang dikirim dan diterima sebelum waktu yang dipilih akan dihapus. Mungkin perlu waktu beberapa menit. + Anda dapat ubah di pengaturan Tampilan. + Grup ini tidak ada lagi. + Anda menolak undangan grup + Anda mengubah hak Anda menjadi %s + Anda mengubah hak %s menjadi %s + Lihat ketentuan + Server untuk berkas baru dari profil obrolan Anda saat ini + Perbarui aplikasi secara otomatis + Operator prasetel kedua dalam aplikasi! + Lihat ketentuan yang diperbarui + Nama perangkat akan dibagikan dengan klien seluler yang terhubung. + Batas waktu tercapai saat menghubungkan ke desktop + Anda sudah terhubung melalui tautan 1-kali ini! + Gunakan dari desktop + Tindakan ini tidak dapat dibatalkan - semua berkas dan media yang diterima dan dikirim akan dihapus. Gambar beresolusi rendah akan tetap ada. + Hak akan diubah menjadi %s. Semua orang dalam grup akan diberitahu. + Basis data obrolan Anda tidak dienkripsi - setel frasa sandi untuk melindunginya. + Anda diundang ke grup + Menunggu ponsel terhubung: + Setel frasa sandi basis data + Perbarui + Perbarui frasa sandi basis data + Anda menghapus %1$s + %s, %s dan %d anggota + %s: %s + Perbarui + Verifikasi koneksi + Berlangganan + Gagal simpan basis data + Frasa sandi disimpan di pengaturan sebagai teks biasa. + Video tidak dapat didekodekan. Silakan coba video lain atau hubungi pengembang. + Enkripsi berfungsi dan perjanjian enkripsi baru tidak diperlukan. Hal ini dapat mengakibatkan kesalahan koneksi! + Bagikan alamat secara publik + Bagikan alamat SimpleX di media sosial. + Bagikan tautan 1-kali dengan teman + Alamat SimpleX dan tautan 1-kali aman untuk dibagikan ke messenger lain. + Anda dapat gunakan markdown untuk format pesan: + Anda akan berhenti menerima pesan dari obrolan ini. Riwayat obrolan akan disimpan. + Gunakan untuk pesan + Gunakan untuk berkas + Koneksi TCP + Anda masih akan menerima panggilan dan notifikasi dari profil yang dibisukan ketika profil tersebut aktif. + Saat Anda berbagi profil samaran dengan seseorang, profil ini akan digunakan untuk grup tempat Anda diundang. + Tautan ini digunakan dengan perangkat seluler lain, silakan buat tautan baru di desktop. + Kontak Anda akan tetap terhubung. + Putuskan desktop? + Peringatan: Anda akan kehilangan beberapa data! + Kesalahan berlangganan + Pesan akan dihapus untuk semua anggota. + Verifikasi kode di ponsel + Anda diundang ke grup. Bergabung untuk terhubung dengan anggota grup. + Profil Anda disimpan di perangkat dan hanya dibagikan dengan kontak. Server SimpleX tidak dapat melihat profil Anda. + Anda mengubah alamat untuk %s + profil grup diperbarui + %s, %s dan %s terhubung + Hak akan diubah menjadi %s. Semua orang dalam obrolan akan diberitahu. + minggu + Gagal unggah + Mengunggah arsip + Anda tidak terhubung ke server ini. Routing pribadi digunakan untuk kirim pesan ke server ini. + Video akan diterima saat kontak Anda online, harap tunggu atau periksa nanti! + Verifikasi kode keamanan + Anda masih dapat melihat percakapan dengan %1$s dalam daftar obrolan. + Pesan suara dilarang! + Tombol ketuk + Batal favorit + Anda mengundang kontak + Alamat SimpleX + ingin terhubung dengan Anda! + Lihat kode QR + Bagikan alamat dengan kontak? + 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. + Kesalahan tidak diketahui + Kesalahan basis data tidak diketahui: %s + Frase sandi salah! + Anda telah meminta koneksi melalui alamat ini! + Hentikan obrolan + Anda tidak dapat kirim pesan! + Pesan suara (%1$s) + Lihat kode keamanan + Anda perlu mengizinkan kontak mengirim pesan suara agar dapat mengirimkannya. + (untuk dibagikan dengan kontak Anda) + Ketuk untuk pindai + Terima kasih telah memasang SimpleX Chat! + Setel nama kontak + Kontak Anda harus online agar koneksi dapat selesai.\nAnda dapat batalkan koneksi ini dan hapus kontak (dan coba lagi nanti dengan tautan baru). + Logo SimpleX + Tim SimpleX + Anda akan terhubung saat perangkat kontak Anda online, harap tunggu atau periksa nanti! + Anda akan terhubung saat permintaan koneksi Anda diterima. Harap tunggu atau periksa nanti! + Bagikan tautan 1-kali + Saat orang meminta untuk terhubung, Anda dapat terima atau menolaknya. + Anda tidak akan kehilangan kontak jika menghapus alamat Anda nanti. + Matikan? + Hentikan obrolan? + Anda bergabung ke grup ini + Hentikan obrolan untuk ekspor, impor, atau hapus basis data obrolan. Anda tidak akan dapat terima dan kirim pesan saat obrolan dihentikan. + Pengaturan ini berlaku untuk pesan di profil obrolan Anda saat ini + Basis data obrolan Anda + buka blokir %s + Anda mengubah alamat + Untuk kirim + Menghentikan obrolan + Perlihat profil + Anda harus masukkan frasa sandi setiap aplikasi dibuka - frasa sandi tidak disimpan di perangkat. + Server SMP + Profil obrolan Anda akan dikirim\nke kontak Anda + Bagikan alamat + Hak akan diubah menjadi %s. Anggota akan menerima undangan baru. + Perbarui pengaturan jaringan? + Ketuk untuk aktifkan profil. + Mulai dari %s. + Anda akan terhubung ke grup saat perangkat pemilik grup sedang online, harap tunggu atau periksa nanti! + Lihat: + Frasa sandi basis data salah + setel foto profil baru + profil diperbarui + Ukuran + Berkas diunggah + Koneksi mencapai batas pesan yang tidak terkirim, kontak Anda mungkin sedang offline. + Kunci SimpleX tidak diaktifkan! + Anda dapat aktifkan Kunci SimpleX di Pengaturan. + Kontak yang Anda bagikan tautan ini TIDAK akan dapat terhubung! + Gambar tidak dapat didekodekan. Silakan coba gambar lain atau hubungi pengembang. + Kunci salah atau koneksi tidak dikenal - kemungkinan besar koneksi ini dihapus. + Mulai obrolan baru + Ketuk Buat alamat SimpleX di menu untuk membuatnya nanti. + Untuk terhubung, kontak Anda dapat pindai kode QR atau gunakan tautan di aplikasi. + Profil Anda %1$s akan dibagikan. + Untuk melindungi tautan Anda dari penggantian, Anda dapat membandingkan kode keamanan kontak. + Pembaruan unduhan dibatalkan + Profil Anda saat ini + Anda menggunakan profil samaran untuk grup ini - untuk mencegah berbagi profil utama Anda, undang kontak tidak diizinkan + Lihat daftar obrolan di jendela baru + Anda mengirim undangan grup + Tautan ini bukan tautan koneksi yang valid! + Anda akan berhenti menerima pesan dari grup ini. Riwayat obrolan akan disimpan. + Anda bergabung ke grup ini. Menghubungkan untuk undang anggota grup. + Anda buka blokir %s + Menunggu desktop… + Ganti profil obrolan untuk undangan 1-kali. + Untuk perlihat profil tersembunyi Anda, masukkan kata sandi lengkap di kolom pencarian di halaman Profil obrolan Anda. + Anda mengendalikan obrolan Anda! + Untuk terima + Perlihat + Perbarui pengaturan akan menghubungkan ulang klien ke semua server. + Verifikasi frasa sandi basis data + Server XFTP + Langganan diabaikan + Berhenti berbagi + Berhenti berbagi alamat? + Tampilkan opsi pengembang + Untuk mengizinkan aplikasi seluler terhubung ke desktop, buka port ini di firewall Anda, jika Anda mengaktifkannya + coret + Hapus pesan setelah + a + b + Hapus obrolan profil untuk + Koneksi belum siap + Kesalahan pada pembuatan daftar percakapan + Kesalahan memuat daftar percakapan + Kesalahan memperbaharui daftar percakapan + Daftar + Kontak + Favorit + Tidak ada pesan + Tidak ditemukan pesan + Tidak ada pesan dalam daftar %s + Tidak ada chat belum terbaca + Tambahkan daftar + Bisnis + Grup + Buka dengan %s + Tambahkan dalam daftar + Buat daftar + Daftar nama + Daftar nama dan emoji harus berbeda untuk semua daftar + Simpan daftar + Semua percakapan akan dipindahkan dari daftar %s, dan daftar akan dihapus + Hapus + Hapus daftar + Sunting + Semua + 1 laporan + Arsip + Alasan lain + laporan arsip oleh %s + Arsip laporan + Hapus laporan + Laporan + Laporkan lainnya: hanya moderator grup yang akan melihat. + Gagal membuat laporan + Koneksi diblokir + Koneksi diblokir oleh operator server:\n%1$s. + Konten melanggar ketentuan penggunaan + Spam + Alasan laporan? + Laporkan + Laporan Anggota + Catatan + Laporkan spam: hanya moderator grup yang akan melihat. + Laporkan pelanggaran: hanya moderator grup yang akan melihat. + Gagal simpan pengaturan + %d laporan + Laporkan profil anggota: hanya moderator grup yang akan melihat. + Arsip laporan? + Berkas diblokir oleh operator server:\n\n%1$s. + Laporan akan diarsipkan untuk Anda. + moderator + Laporkan konten: hanya moderator grup yang akan melihat. + Ubah daftar + Ubah urutan + laporan arsip + Pelanggaran pedoman komunitas + Konten tidak pantas + Profil tidak pantas + Hanya pengirim dan moderator dapat melihat + Hanya Anda dan moderator dapat melihat + Spam + Setel nama obrolan… + Buka tautan peramban? + Buka tautan + Ubah hapus pesan otomatis? + Matikan hapus pesan otomatis? + Pesan dalam obrolan ini tidak akan pernah dihapus. + Hapus pesan obrolan dari perangkat Anda. + Matikan hapus pesan + Tindakan tak dapat dibatalkan - pesan yang dikirim dan diterima dalam obrolan ini sebelum yang dipilih akan dihapus. + Tanya + Tidak + Buka tautan dari daftar obrolan + Ya + 1 tahun + bawaan (%s) + Bisukan semua + Sebutan belum terbaca + Port TCP untuk pesan + Gunakan port TCP %1$s jika tidak ada port yang ditentukan. + Gunakan port peramban + Anda dapat menyebut hingga %1$s anggota per pesan! + Semua laporan akan diarsipkan untuk Anda. + Arsipkan semua laporan? + Untuk semua moderator + Untuk saya + Arsip laporan + Laporan: %s + Izinkan untuk laporkan pesan ke moderator. + Anggota dapat melaporkan pesan ke moderator. + Laporkan pesan dilarang di grup ini. + Dilarang laporkan pesan ke moderator. + Arsipkan %d laporan? + Gagal membaca frasa sandi basis data + Privasi dan keamanan lebih baik + Semua pesan baru dari anggota ini akan disembunyikan! + Blokir anggota untuk semua? + Dapat notifikasi saat disebut. + Kirim pesan lebih cepat. + Jangan lewatkan pesan penting. + Kinerja grup yang lebih baik + Hapus grup lebih cepat. + Syarat diperbarui + Anggota akan dihapus dari obrolan - hal ini tidak dapat dibatalkan! + Hapus anggota? + ditolak + tertunda + menunggu persetujuan + Buka blokir anggota untuk semua? + Kirim laporan pribadi + Bantu admin memoderasi grup. + Atur pesan kedaluwarsa obrolan. + Nama berkas media pribadi. + ditolak + Anggota akan dihapus dari grup - tindakan ini tidak dapat dibatalkan! + moderator + Pesan dari anggota ini akan ditampilkan! + Sebutkan anggota 👋 + Atur obrolan ke dalam daftar + Frasa sandi di Keystore tidak dapat dibaca. Hal ini mungkin terjadi setelah pembaruan sistem yang tidak kompatibel dengan aplikasi. Jika tidak demikian, silakan hubungi pengembang. + Terima + Dengan menggunakan SimpleX Chat, Anda setuju untuk:\n- hanya mengirim konten legal di grup publik.\n- hormati pengguna lain – tidak ada spam. + Konfigurasikan operator server + Kebijakan privasi dan ketentuan penggunaan. + Obrolan pribadi, grup, dan kontak Anda tidak dapat diakses oleh operator server. + Frasa sandi di Keystore tidak dapat dibaca, silakan masukkan secara manual. Hal ini mungkin terjadi setelah pembaruan sistem yang tidak kompatibel dengan aplikasi. Jika tidak demikian, silakan hubungi pengembang. + Gunakan port TCP 443 hanya untuk presetel server. + Semua server + Mati + Presetel server + Tautan lengkap + Tautan ini perlu versi aplikasi yang baru. Harap perbarui aplikasi atau minta kontak untuk kirim tautan kompatibel. + Tautan saluran SimpleX + Gunakan tautan singkat (BETA) + Tautan koneksi tidak didukung + Tautan singkat + 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 310a021ff2..6c086835ea 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -211,8 +211,8 @@ 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. Probabilmente l\'impronta del certificato nell\'indirizzo del server è sbagliata - 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. - Per usarlo, <b>consenti a SimpleX di funzionare in secondo piano</b> nella prossima schermata. Altrimenti le notifiche saranno disattivate. + Per migliorare la privacy, <b>SimpleX funziona in secondo piano</b> invece di usare le notifiche push. + <b>Consentilo</b> nella prossima schermata per ricevere le notifiche immediatamente. Servizio SimpleX Chat Servizio in secondo piano sempre attivo. Le notifiche verranno mostrate appena i messaggi saranno disponibili. SimpleX Lock @@ -249,7 +249,7 @@ Accetta in incognito Tutti i messaggi verranno eliminati, non è reversibile! I messaggi verranno eliminati SOLO per te. Aggiungi server preimpostati - Aggiungi server… + Aggiungi server Impostazioni di rete avanzate Riguardo SimpleX chiamata accettata @@ -266,18 +266,17 @@ Nome completo del gruppo: 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. Backup dei dati dell\'app - Android Keystore è usato per memorizzare in modo sicuro la password; permette il funzionamento del servizio di notifica. + L\'archivio chiavi di Android è usato per memorizzare in modo sicuro la password; permette il funzionamento del servizio di notifica. Permetti ai tuoi contatti di inviare messaggi vocali. Database della chat eliminato ICONA APP <b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione (NO servizio in secondo piano). - <b>Consuma più batteria</b>! Servizio in secondo piano sempre attivo: le notifiche sono mostrate non appena i messaggi sono disponibili. + <b>Consuma più batteria</b>! L\'app funziona sempre in secondo piano: le notifiche vengono mostrate istantaneamente. chiamata… annulla anteprima link - Impossibile accedere al Keystore per salvare la password del database + Impossibile accedere all\'archivio chiavi per salvare la password del database Impossibile invitare i contatti! Cambia ruolo - ARCHIVIO CHAT cambio indirizzo… Chat fermata in connessione (presentato) @@ -286,11 +285,11 @@ Elimina link Crea indirizzo Crea link - La password di crittografia del database verrà aggiornata e conservata nel Keystore. + La password di crittografia del database verrà aggiornata e conservata nell\'archivio chiavi. Il database è crittografato con una password casuale, puoi cambiarla. La password del database è necessaria per aprire la chat. Elimina - I messaggi diretti tra i membri sono vietati in questo gruppo. + I messaggi diretti tra i membri sono vietati in questo gruppo. Inserisci il tuo nome: File Svuota chat @@ -351,7 +350,7 @@ Come usare il markdown chiamata audio chiamata audio (non crittografata e2e) - <b>Buono per la batteria</b>. Il servizio in secondo piano cerca messaggi ogni 10 minuti. Potresti perdere chiamate o messaggi urgenti. + <b>Buono per la batteria</b>. L\'app cerca messaggi ogni 10 minuti. Potresti perdere chiamate o messaggi urgenti. Chiamata già terminata! Crea il tuo profilo Decentralizzato @@ -364,7 +363,7 @@ Audio spento Audio acceso Chiamate audio e video - Auto-accetta immagini + Auto-accetta le immagini hash del messaggio errato ID messaggio errato Chiamata terminata @@ -398,13 +397,9 @@ Funzionalità sperimentali Esporta database AIUTO - Archivio chat Chat fermata - Creato il %1$s Errore del database - La password del database è diversa da quella salvata nel Keystore. - Elimina archivio - Eliminare l\'archivio della chat\? + La password del database è diversa da quella salvata nell\'archivio chiavi. Database crittografato Inserisci la password giusta. Inserisci la password… @@ -413,7 +408,7 @@ Gruppo inattivo indirizzo cambiato per te ha cambiato il ruolo di %s in %s - cambiato il tuo ruolo in %s + ha cambiato il tuo ruolo in %s cambio indirizzo… cambio indirizzo per %s… si è connesso/a @@ -475,7 +470,7 @@ attivato per il contatto attivato per te Preferenze del gruppo - Auto-accetta richieste di contatto + Auto-accetta le richieste di contatto %dg %d giorno %d giorni @@ -483,7 +478,7 @@ %do %d ora %d ore - I messaggi a tempo sono vietati in questo gruppo. + I messaggi a tempo sono vietati. %dm %d min %d mese @@ -495,14 +490,14 @@ %d settimana %d settimane Link del gruppo - I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore) - I membri del gruppo possono inviare messaggi diretti. - I membri del gruppo possono inviare messaggi a tempo. - I membri del gruppo possono inviare messaggi vocali. + I membri possono eliminare irreversibilmente i messaggi inviati. (24 ore) + I membri possono inviare messaggi diretti. + I membri possono inviare messaggi a tempo. + I membri possono inviare messaggi vocali. Confronta i codici di sicurezza con i tuoi contatti. Messaggi a tempo Nascondi la schermata dell\'app nelle app recenti. - 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. + 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. <b>Nota bene</b>: NON potrai recuperare o cambiare la password se la perdi. Cambiare password del database\? Conferma nuova password… @@ -510,7 +505,7 @@ Database crittografato! La password di crittografia del database verrà aggiornata. Il database verrà crittografato. - Il database verrà crittografato e la password conservata nel Keystore. + Il database verrà crittografato e la password conservata nell\'archivio chiavi. Eliminare i file e i multimediali\? Elimina messaggi Elimina messaggi dopo @@ -543,7 +538,7 @@ Codice QR non valido immagine di anteprima link Segna come già letto - Segna come non letto + Segna come non letta Altro Silenzia immagine del profilo @@ -606,22 +601,18 @@ Installa SimpleX Chat per terminale Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi. Rete e server - Impostazioni di rete + Impostazioni avanzate No Gli host Onion saranno necessari per la connessione. \nNota bene: non potrai connetterti ai server senza indirizzo .onion . - Gli host Onion saranno necessari per la connessione. Gli host Onion verranno usati quando disponibili. - Gli host Onion verranno usati quando disponibili. Gli host Onion non verranno usati. - Gli host Onion non verranno usati. Valuta l\'app Obbligatorio Salva I server WebRTC ICE salvati verranno rimossi. Condividi link Dai una stella su GitHub - Aggiornare l\'impostazione degli host .onion\? Usare una connessione internet diretta\? Usa gli host .onion Usare il proxy SOCKS\? @@ -653,32 +644,29 @@ Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo. Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo. Ignora - Immune a spam e abusi + Immune allo spam Chiamata in arrivo Videochiamata in arrivo Istantaneo - Può essere cambiato in seguito via impostazioni. + Come influisce sulla batteria Crea una connessione privata - Molte persone hanno chiesto: <i>se SimpleX non ha identificatori utente, come può recapitare i messaggi\?</i> - 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>. - Protocollo e codice open source: chiunque può gestire i server. + Solo i dispositivi client memorizzano i profili utente, i contatti, i gruppi e i messaggi. + Chiunque può installare i server. Incolla il link che hai ricevuto - Le persone possono connettersi a te solo tramite i link che condividi. + Sei tu a decidere chi può connettersi. Periodico Privacy ridefinita Notifiche private Maggiori informazioni nel nostro <font color="#0088ff">repository GitHub</font>. - Maggiori informazioni nel nostro repository GitHub. Rifiuta - La prima piattaforma senza alcun identificatore utente – privata by design. - La nuova generazione di messaggistica privata - 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. + Nessun identificatore utente. + Il futuro dei messaggi + Per proteggere la tua privacy, SimpleX usa ID separati per ciascuno dei tuoi contatti. Usa la chat videochiamata videochiamata (non crittografata e2e) Quando l\'app è in esecuzione %1$s vuole connettersi con te via - Tu decidi attraverso quale/i server <b>ricevere</b> i messaggi, i tuoi contatti quali server usi per inviare loro i messaggi. 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. @@ -716,7 +704,7 @@ Riavvia l\'app per creare un profilo di chat nuovo. Riavvia l\'app per usare il database della chat importato. AVVIA CHAT - Invia anteprime dei link + Invia le anteprime dei link Imposta la password per esportare IMPOSTAZIONI PROXY SOCKS @@ -741,14 +729,13 @@ Esci Uscire dal gruppo\? Apri chat - 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. + 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. Inserisci la password precedente dopo aver ripristinato il backup del database. Questa azione non può essere annullata. Conserva la password in modo sicuro, NON potrai accedere alla chat se la perdi. Ripristina Ripristina backup del database Ripristinare il backup del database\? Errore di ripristino del database - Salva archivio Salva la password e apri la chat Il tentativo di cambiare la password del database non è stato completato. Errore del database sconosciuto: %s @@ -816,7 +803,6 @@ Scadenza del protocollo Ricezione via Ripristina i predefiniti - Ripristina Salva Salva il profilo del gruppo sec @@ -826,8 +812,8 @@ Sistema Scadenza connessione TCP Completamente decentralizzato: visibile solo ai membri. - Il ruolo verrà cambiato in \"%s\". Tutti i membri del gruppo riceveranno una notifica. - Il ruolo verrà cambiato in \"%s\". Il membro riceverà un nuovo invito. + Il ruolo verrà cambiato in "%s". Tutti i membri del gruppo riceveranno una notifica. + Il ruolo verrà cambiato in "%s". Il membro riceverà un nuovo invito. Aggiorna Aggiornare le impostazioni di rete\? L\'aggiornamento delle impostazioni riconnetterà il client a tutti i server. @@ -847,7 +833,6 @@ Proibisci l\'invio di messaggi vocali. ricevuto, vietato Ripristina i colori - Salva colore Imposta 1 giorno Imposta le preferenze del gruppo Tema @@ -857,7 +842,7 @@ Le tue preferenze Configurazione del server migliorata Eliminazione irreversibile del messaggio - L\'eliminazione irreversibile dei messaggi è vietata in questo gruppo. + L\'eliminazione irreversibile dei messaggi è vietata. Max 40 secondi, ricevuto istantaneamente. Novità nella %s Solo tu puoi inviare messaggi vocali. @@ -870,7 +855,7 @@ La sicurezza di SimpleX Chat è stata verificata da Trail of Bits. Messaggi vocali I messaggi vocali sono vietati in questa chat. - I messaggi vocali sono vietati in questo gruppo. + I messaggi vocali sono vietati. Novità Con messaggio di benvenuto facoltativo. I tuoi contatti possono consentire l\'eliminazione completa dei messaggi. @@ -886,10 +871,9 @@ Inserisci la password attuale corretta. Conserva la password in modo sicuro, NON potrai cambiarla se la perdi. Rimuovi - Rimuovere la password dal Keystore\? - Salva la password nel Keystore + Rimuovere la password dall\'archivio chiavi? + Salva la password nell\'archivio chiavi %s secondo/i - Ferma la chat per attivare le azioni del database. Questa azione non può essere annullata: tutti i file e i media ricevuti e inviati verranno eliminati. Rimarranno le immagini a bassa risoluzione. Questa azione non può essere annullata: i messaggi inviati e ricevuti prima di quanto selezionato verranno eliminati. Potrebbe richiedere diversi minuti. Aggiorna @@ -918,8 +902,7 @@ Aggiornare la modalità di isolamento del trasporto\? Tutte le chat e i messaggi verranno eliminati. Non è reversibile! Profilo di chat - Verrà usata una connessione TCP separata (e le credenziali SOCKS) per ogni contatto e membro del gruppo . -\n Nota: : se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire. + per ogni contatto e membro del gruppo .\n Nota: : se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire.]]> Connessione Questa impostazione si applica ai messaggi del profilo di chat attuale Isolamento del trasporto @@ -935,7 +918,7 @@ Grazie agli utenti – contribuite via Weblate! Interfaccia francese Interfaccia italiana - Bozza dei messaggi + Bozza del messaggio Conserva la bozza dell\'ultimo messaggio, con gli allegati. Nomi di file privati Per profilo di chat (predefinito) o per connessione (BETA). @@ -983,11 +966,11 @@ Grazie agli utenti – contribuite via Weblate! Password del profilo nascosta Salva la password del profilo - Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina \"I tuoi profili di chat\". + Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina "I tuoi profili di chat". Password per mostrare Ora gli amministratori possono: \n- eliminare i messaggi dei membri. -\n- disattivare i membri (ruolo \"osservatore\") +\n- disattivare i membri (ruolo "osservatore")
Nascondi il profilo Conferma password Errore nell\'aggiornamento della privacy dell\'utente @@ -1115,12 +1098,12 @@ Grazie agli utenti – contribuite via Weblate! Video e file fino a 1 GB Accetta automaticamente - Apri i profili di chat + Cambia i profili di chat Maggiori informazioni Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell\'app. Quando le persone chiedono di connettersi, puoi accettare o rifiutare. Indirizzo SimpleX - COLORI DEL TEMA + COLORI DELL\'INTERFACCIA I tuoi contatti resteranno connessi. Aggiungi l\'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L\'aggiornamento del profilo verrà inviato ai tuoi contatti. Crea un indirizzo per consentire alle persone di connettersi con te. @@ -1148,7 +1131,7 @@ Secondario Messaggio ricevuto Messaggio inviato - Titolo + Titoli Link una tantum Info sull\'indirizzo SimpleX Tutti i tuoi contatti resteranno connessi. L\'aggiornamento del profilo verrà inviato ai tuoi contatti. @@ -1186,7 +1169,7 @@ Cambia codice di autodistruzione Reazioni ai messaggi Le reazioni ai messaggi sono vietate in questa chat. - Le reazioni ai messaggi sono vietate in questo gruppo. + Le reazioni ai messaggi sono vietate. Solo tu puoi aggiungere reazioni ai messaggi. Proibisci le reazioni ai messaggi. Proibisci le reazioni ai messaggi. @@ -1195,7 +1178,7 @@ Solo il tuo contatto può aggiungere reazioni ai messaggi. Consenti ai tuoi contatti di aggiungere reazioni ai messaggi. Consenti reazioni ai messaggi solo se il tuo contatto le consente. - I membri del gruppo possono aggiungere reazioni ai messaggi. + I membri possono aggiungere reazioni ai messaggi. 30 secondi Invia Invia messaggio a tempo @@ -1246,7 +1229,7 @@ Scompare il Scompare il: %s nessun testo - Si sono verificati alcuni errori non gravi durante l\'importazione: vedi la console della chat per i dettagli. + Si sono verificati alcuni errori non fatali durante l\'importazione: Riavvia APP Le notifiche smetteranno di funzionare fino a quando non riavvierai l\'app @@ -1259,10 +1242,10 @@ Errore nell\'interruzione del cambio di indirizzo Solo i proprietari del gruppo possono attivare file e contenuti multimediali. File e multimediali - I membri del gruppo possono inviare file e contenuti multimediali. + I membri possono inviare file e contenuti multimediali. Proibisci l\'invio di file e contenuti multimediali. Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione. - File e contenuti multimediali sono vietati in questo gruppo. + File e contenuti multimediali sono vietati. File e contenuti multimediali vietati! Off Nessuna chat filtrata @@ -1365,7 +1348,7 @@ %s e %s si sono connessi/e %s, %s e altri %d membri si sono connessi %s, %s e %s si sono connessi/e - Bozza + Bozza del messaggio Mostra gli ultimi messaggi Il database verrà crittografato e la password conservata nelle impostazioni. La password casuale viene conservata nelle impostazioni come testo normale. @@ -1603,7 +1586,7 @@ Errore di eliminazione delle note private Consegna dei messaggi migliorata Entra in conversazioni di gruppo - membro %1$s cambiato in %2$s + il membro %1$s è diventato %2$s Incolla un link per connettere! indirizzo di contatto rimosso contatto %1$s cambiato in %2$s @@ -1612,7 +1595,7 @@ immagine del profilo rimossa Con file e multimediali criptati. La barra di ricerca accetta i link di invito. - impostata nuova immagine del profilo + ha impostato una nuova immagine del profilo impostato nuovo indirizzo di contatto profilo aggiornato Messaggio salvato @@ -1632,8 +1615,7 @@ hai bloccato %s Il messaggio di benvenuto è troppo lungo Messaggio troppo grande - Migrazione database in corso. -\nPuò richiedere qualche minuto. + Migrazione del database in corso.\nPuò richiedere qualche minuto. Chiamata audio Termina chiamata Videochiamata @@ -1738,11 +1720,11 @@ Consenti di inviare link di SimpleX. Vieta l\'invio di link di SimpleX Attivo per - I membri del gruppo possono inviare link di Simplex. + I membri possono inviare link di Simplex. proprietari amministratori tutti i membri - I link di SimpleX sono vietati in questo gruppo. + I link di SimpleX sono vietati. salvato Salvato da Inoltra messaggio… @@ -1769,4 +1751,644 @@ La fonte del messaggio resta privata. Forma delle immagini del profilo Quadrata, circolare o qualsiasi forma tra le due - \ No newline at end of file + Server di inoltro: %1$s +\nErrore del server di destinazione: %2$s + Server di inoltro: %1$s +\nErrore: %2$s + Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo. + L\'indirizzo del server non è compatibile con le impostazioni di rete. + La versione del server non è compatibile con le impostazioni di rete. + Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata. + Errore del server di destinazione: %1$s + Errore: %1$s + Quota superata - il destinatario non ha ricevuto i messaggi precedentemente inviati. + Avviso di consegna del messaggio + Instradamento privato + Mai + Server sconosciuti + Usa l\'instradamento privato con server sconosciuti. + Modalità instradamento messaggio + Usa l\'instradamento privato con server sconosciuti quando l\'indirizzo IP non è protetto. + + Invia messaggi direttamente quando il tuo server o quello di destinazione non supporta l\'instradamento privato. + Quando l\'IP è nascosto + Ripiego instradamento messaggio + Mostra stato del messaggio + Consenti downgrade + Sempre + Usa sempre l\'instradamento privato. + NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l\'instradamento privato. + NON usare l\'instradamento privato. + No + INSTRADAMENTO PRIVATO DEI MESSAGGI + Invia messaggi direttamente quando l\'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l\'instradamento privato. + Per proteggere il tuo indirizzo IP, l\'instradamento privato usa i tuoi server SMP per consegnare i messaggi. + Non protetto + Server sconosciuti! + Proteggi l\'indirizzo IP + L\'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion o quando il proxy SOCKS è attivo). + Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file. + FILE + Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: +\n%1$s. + Tema della chat + Nero + Modalità di colore + Scura + Modalità scura + Principale aggiuntivo 2 + Tutte le modalità di colore + Applica a + Colori modalità scura + Tema del profilo + Risposta ricevuta + Colori della chat + Riempi + Chiara + Modalità chiara + Ripristina colore + Impostazioni avanzate + Rimuovi immagine + Ripeti + Scala + Adatta + Risposta inviata + Imposta tema predefinito + Mostra la lista di chat in una nuova finestra + Sistema + Tinta dello sfondo + Buon pomeriggio! + Buongiorno! + Retro dello sfondo + Instradamento privato dei messaggi 🚀 + Proteggi il tuo indirizzo IP dai relay di messaggistica scelti dai tuoi contatti. +\nAttivalo nelle impostazioni *Rete e server*. + Errore di inizializzazione di WebView. Aggiorna il sistema ad una nuova versione. Contatta gli sviluppatori. +\nErrore: %s + Tema dell\'app + Ripristina al tema dell\'app + Ripristina al tema dell\'utente + Conferma i file da server sconosciuti. + Consegna dei messaggi migliorata + Cambia l\'aspetto delle tue chat! + Nuovi temi delle chat + Interfaccia in persiano + Ricevi i file in sicurezza + Con consumo di batteria ridotto. + Info coda messaggi + nessuna + info coda server: %1$s +\n +\nultimo msg ricevuto: %2$s + Debug della consegna + Stato del messaggio: %s + Stato del file: %s + Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato. + File non trovato - probabilmente è stato eliminato o annullato. + Stato del messaggio + Stato del file + Errore del server del file: %1$s + Errore del file + Errore del file temporaneo + 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. + Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop. + Copia errore + Impossibile inviare il messaggio + Le preferenze della chat selezionata vietano questo messaggio. + Connessioni + Connessioni attive + Creato + errori di decifrazione + Statistiche dettagliate + doppi + Errore + Errore di riconnessione al server + Errore di riconnessione ai server + scaduto + Ricezione messaggi + altro + altri errori + In attesa + Via proxy + Server via proxy + Totale ricevuto + Errori di ricezione + Riconnetti + Riconnetti tutti i server connessi per forzare la consegna dei messaggi. Usa traffico aggiuntivo. + Riconnettere il server? + Riconnettere i server? + Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo. + Errori di invio + Inviato direttamente + Messaggi inviati + Inviato via proxy + Le statistiche dei server verranno azzerate - è irreversibile! + Azzera tutte le statistiche + Azzerare tutte le statistiche? + Azzera + Errore di azzeramento statistiche + Server SMP + Inizio da %s. + Totale + Inviato + Server XFTP + Riconosciuto + Errori di riconoscimento + Blocchi eliminati + Blocchi scaricati + Blocchi inviati + Eliminato + Errori di eliminazione + File scaricati + Apri impostazioni server + Protetto + Indirizzo server + Dimensione + Iscritto + Errori di iscrizione + Iscrizioni ignorate + File inviati + Errori di invio + Errore di instradamento privato + La versione del server non è compatibile con la tua app: %1$s. + Membro inattivo + Il messaggio può essere consegnato più tardi se il membro diventa attivo. + Server SMP configurati + Altri server SMP + Mostra percentuale + inattivo + Zoom + Connesso + In connessione + Profilo attuale + Dettagli + Errori + Messaggi ricevuti + Messaggi inviati + Nessuna informazione, prova a ricaricare + Info dei server + Informazioni di + Statistiche + Sessioni di trasporto + Tutti i profili + tentativi + Server XFTP configurati + Completato + Server connessi + disattivato + Scaricato + Messaggi ricevuti + Errori di scaricamento + Riconnetti tutti i server + L\'indirizzo del server non è compatibile con le impostazioni di rete: %1$s. + File + Scansiona / Incolla link + Dimensione carattere + Totale inviato + Messaggio inoltrato + Partendo da %s. \nTutti i dati sono privati, nel tuo dispositivo. + Ancora nessuna connessione diretta, il messaggio viene inoltrato dall\'amministratore. + Non sei connesso/a a questi server. L\'instradamento privato è usato per consegnare loro i messaggi. + Altri server XFTP + Server precedentemente connessi + Riprova più tardi. + Beta + Disattiva + Disattivato + Scaricamento dell\'aggiornamento, non chiudere l\'app + Scarica %s (%s) + Installato correttamente + Apri percorso file + Riavvia l\'app. + Ricordamelo più tardi + Salta questa versione + Stabile + Per essere avvisato sulle nuove versioni, attiva il controllo periodico di versioni stabili o beta. + Aggiornamento disponibile: %s + Scaricamento aggiornamento annullato + Cerca aggiornamenti + Aggiornamento dell\'app scaricato + Cerca aggiornamenti + Installa aggiornamento + Il server di inoltro %1$s non è riuscito a connettersi al server di destinazione %2$s. Riprova più tardi. + L\'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %1$s. + L\'indirizzo del server di destinazione di %1$s è incompatibile con le impostazioni del server di inoltro %2$s. + La versione del server di destinazione di %1$s è incompatibile con il server di inoltro %2$s. + Errore di connessione al server di inoltro %1$s. Riprova più tardi. + La versione server di inoltro è incompatibile con le impostazioni di rete: %1$s. + Off + Sfocatura dei file multimediali + Leggera + Media + Forte + chiama + messaggio + apri + cerca + Impostazioni + Elimina senza avvisare + Tieni la conversazione + Elimina solo la conversazione + Puoi inviare messaggi a %1$s dai contatti archiviati. + Contatti archiviati + Nessun contatto filtrato + Incolla link + I tuoi contatti + Barre degli strumenti dell\'app accessibili + Invita + Consentire le chiamate? + Chiamate proibite! + Impossibile chiamare il contatto + Impossibile chiamare il membro del gruppo + In collegamento con il contatto, attendi o controlla più tardi! + Il contatto è stato eliminato. + Chiedi al contatto di attivare le chiamate. + Devi consentire le chiamate al tuo contatto per poterlo chiamare. + Impossibile inviare messaggi al membro del gruppo + Il contatto verrà eliminato - non è reversibile! + Conversazione eliminata! + Invia un messaggio per attivare le chiamate. + video + Puoi ancora vedere la conversazione con %1$s nell\'elenco delle chat. + connetti + Contatto eliminato! + Confermare l\'eliminazione del contatto? + Messaggio + Nessuna selezione + Seleziona + Selezionato %d + I messaggi verranno eliminati per tutti i membri. + I messaggi verranno contrassegnati come moderati per tutti i membri. + Eliminare %d messaggi dei membri? + I messaggi saranno contrassegnati per l\'eliminazione. Il/I destinatario/i sarà/saranno in grado di rivelare questi messaggi. + Database della chat esportato + Continua + Server di multimediali e file + Server dei messaggi + Proxy SOCKS + Alcuni file non sono stati esportati + Puoi migrare il database esportato. + Puoi salvare l\'archivio esportato. + Connessione TCP + Connettiti più velocemente ai tuoi amici + Prendi il controllo della tua rete + Elimina fino a 20 messaggi contemporaneamente. + Barra degli strumenti di chat accessibile + Archivia contatti per chattare più tardi. + Usa l\'app con una mano sola. + Stato della connessione e dei server. + Protegge il tuo indirizzo IP e le connessioni. + Salva e riconnetti + Ripristina tutti i suggerimenti + Cambia l\'elenco delle chat: + Puoi cambiarlo nelle impostazioni dell\'aspetto. + Riproduci dall\'elenco delle chat. + Aumenta la dimensione dei caratteri. + Aggiorna l\'app automaticamente + Invita + Sfoca per una privacy maggiore. + Una nuova esperienza di chat 🎉 + Nuove opzioni multimediali + Crea + Scarica nuove versioni da GitHub. + Nuovo messaggio + Link non valido + Controlla che il link SimpleX sia corretto. + Errore nel cambio di profilo + Seleziona il profilo di chat + Condividi il profilo + DATABASE DELLA CHAT + Modalità di sistema + Rimuovere l\'archivio? + I messaggi verranno eliminati. Non è reversibile! + L\'archivio del database caricato verrà rimosso definitivamente dai server. + La tua connessione è stata spostata a %s, ma si è verificato un errore imprevisto durante il reindirizzamento al profilo. + Non usare credenziali con proxy. + Assicurati che la configurazione del proxy sia corretta. + Autenticazione del proxy + Usa diverse credenziali del proxy per ogni connessione. + Usa diverse credenziali del proxy per ogni profilo. + Usa credenziali casuali + Le credenziali potrebbero essere inviate in chiaro. + Errore di salvataggio del proxy + Password + Nome utente + %1$d file è/sono ancora in scaricamento. + %1$s messaggi non inoltrati + %1$d altro/i errore/i di file. + Errore nell\'inoltro dei messaggi + %1$d errore/i di file: +\n%2$s + Inoltrare %1$s messaggio/i? + Inoltrare i messaggi senza file? + I messaggi sono stati eliminati dopo che li hai selezionati. + Niente da inoltrare! + %1$d file ha/hanno fallito lo scaricamento. + %1$d file è/sono stato/i eliminato/i. + %1$d file non è/sono stato/i scaricato/i. + Scarica + Inoltro di %1$s messaggi + Inoltra messaggi… + Salvataggio di %1$s messaggi + Audio silenziato + Errore di inizializzazione di WebView. Assicurati di avere WebView installato e che la sua architettura supportata sia arm64. +\nErrore: %s + Angolo + Forma del messaggio + Coda + Sessione dell\'app + Le nuove credenziali SOCKS verranno usate ogni volta che avvii l\'app. + Le nuove credenziali SOCKS verranno usate per ogni server. + Server + Apri le impostazioni di Safari / Siti web / Microfono, quindi scegli Consenti per localhost. + Clicca il pulsante info vicino al campo indirizzo per consentire l\'uso del microfono. + Per effettuare chiamate, consenti di usare il microfono. Termina la chiamata e cerca di richiamare. + Chiamate migliorate + Date dei messaggi migliorate. + Sicurezza migliorata ✅ + Esperienza utente migliorata + Forma dei messaggi personalizzabile. + Protocolli di SimpleX esaminati da Trail of Bits. + Cambia tra audio e video durante la chiamata. + Cambia profilo di chat per inviti una tantum. + Elimina o modera fino a 200 messaggi. + Inoltra fino a 20 messaggi alla volta. + Nessun server dei messaggi. + Nessun server per ricevere messaggi. + Errori nella configurazione dei server. + Per il profilo di chat %s: + Messaggi non consegnati + Nessun messaggio + Sicurezza della connessione + L\'indirizzo SimpleX e i link una tantum sono sicuri da condividere tramite qualsiasi messenger. + Per proteggerti dalla sostituzione del tuo link, puoi confrontare i codici di sicurezza del contatto. + Puoi impostare il nome della connessione per ricordare con chi è stato condiviso il link. + Condividi link una tantum con un amico + Crea link una tantum + Per i social media + O per condividere in modo privato + Operatori di rete + L\'app protegge la tua privacy usando diversi operatori per ogni conversazione. + Puoi configurare gli operatori nelle impostazioni di rete e server. + Operatori del server + Seleziona gli operatori di rete da usare. + Continua + Aggiorna + Leggi più tardi + Server preimpostati + Condizioni accettate + Le condizioni verranno accettate automaticamente per gli operatori attivati il: %s. + I tuoi server + Leggi le condizioni + %s.]]> + %s.]]> + Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link: + Operatore di rete + Server di %s + Usa %s + Sito web + %s.]]> + Condizioni accettate il: %s. + Operatore + %s.]]> + %s.]]> + %s.]]> + %s.]]> + Accetta le condizioni + Errore di aggiornamento del server + Per l\'instradamento privato + I server per nuovi file del tuo profilo di chat attuale + Per ricevere + Per inviare + Usa per i messaggi + Vedi le condizioni + %s.]]> + %s, accetta le condizioni d\'uso.]]> + Condizioni d\'uso + Apri le modifiche + Apri le condizioni + Il protocollo del server è cambiato. + Errore di aggiunta del server + Server dell\'operatore + Server aggiunto all\'operatore %s. + L\'operatore del server è cambiato. + Barre degli strumenti + Trasparenza + Decentralizzazione della rete + Il secondo operatore preimpostato nell\'app! + Attiva Flux nelle impostazioni \"Rete e server\" per una migliore privacy dei metadati. + Vedi le condizioni aggiornate + Sfocatura + Server dei messaggi aggiunti + Server di multimediali e file aggiunti + Indirizzo o link una tantum? + Impostazioni dell\'indirizzo + con un solo contatto - condividilo di persona o tramite un messenger.]]> + Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni. + Le condizioni verranno accettate il: %s. + Errore di accettazione delle condizioni + Errore di salvataggio dei server + per una migliore privacy dei metadati. + 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. + Navigazione della chat migliorata + Nuovo server + Usa per i file + Indirizzo SimpleX o link una tantum? + Questo messaggio è stato eliminato o non ancora ricevuto. + Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi. + La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline. + Usa i server + Puoi configurare i server nelle impostazioni. + Nessun server di multimediali e file. + Nessun server per l\'instradamento dei messaggi privati. + Nessun server per ricevere file. + Nessun server per inviare file. + - Apri la chat sul primo messaggio non letto.\n- Salta ai messaggi citati. + Condividi indirizzo pubblicamente + Condividi l\'indirizzo SimpleX sui social media. + O importa file archivio + Telefoni remoti + I messaggi diretti tra i membri sono vietati in questa chat. + Dispositivi Xiaomi: attiva l\'avvio automatico nelle impostazioni di sistema per fare funzionare le notifiche.]]> + Aggiungi i membri del tuo team alle conversazioni. + Indirizzo di lavoro + cifrati end-to-end, con sicurezza quantistica nei messaggi diretti.]]> + Controlla i messaggi ogni 10 minuti + Come aiuta la privacy + Invita in chat + Aggiungi membri del team + Chat + I messaggi diretti tra i membri sono vietati. + La chat esiste già! + %1$s.]]> + Uscire dalla chat? + La chat verrà eliminata solo per te, non è reversibile! + Esci dalla chat + Chat di lavoro + La chat verrà eliminata per tutti i membri, non è reversibile! + Aggiungi amici + L\'app funziona sempre in secondo piano + Elimina chat + Eliminare la chat? + Il messaggio è troppo grande! + Riduci la dimensione del messaggio e invialo di nuovo. + Riduci la dimensione del messaggio o rimuovi i media e invialo di nuovo. + Nessun servizio in secondo piano + Il membro verrà rimosso dalla chat, non è reversibile! + Privacy per i tuoi clienti. + Barra degli strumenti di chat accessibile + Solo i proprietari della chat possono modificarne le preferenze. + Notifiche e batteria + Puoi copiare e ridurre la dimensione del messaggio per inviarlo. + Il ruolo verrà cambiato in %s. Verrà notificato a tutti nella chat. + Il tuo profilo di chat verrà inviato ai membri della chat + Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata. + Quando più di un operatore è attivato, nessuno di essi ha metadati per capire chi comunica con chi. + invito accettato + richiesto di connettersi + SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell\'app. + Info sugli operatori + La connessione richiede la rinegoziazione della crittografia. + Correggi + Correggere la connessione? + Rinegoziazione della crittografia in corso. + Attiva i log + Errore di salvataggio del database + Connessione non pronta. + Errore di aggiornamento dell\'elenco di chat + Tutte + Preferite + Gruppi + Nessuna chat + Nessuna chat trovata + Nessuna chat non letta + Aggiungi elenco + Apri con %s + Aggiungi ad un elenco + Nome elenco... + Salva elenco + Elimina + Tutte le chat verranno rimosse dall\'elenco %s, e l\'elenco eliminato + Eliminare l\'elenco? + Il nome dell\'elenco e l\'emoji dovrebbero essere diversi per tutte le liste. + Errore di caricamento dell\'elenco di chat + Errore di creazione dell\'elenco di chat + Elenco + Contatti + Lavorative + Crea elenco + Modifica + Nessuna chat nell\'elenco %s. + Cambia elenco + Note + Cambia ordine + Errore di salvataggio delle impostazioni + Errore nella creazione del resoconto + Archiviare la segnalazione? + segnalazione archiviata + Segnala contenuto: solo i moderatori del gruppo lo vedranno. + Archivia + Altro motivo + Segnala altro: solo i moderatori del gruppo lo vedranno. + Violazione delle linee guida della comunità + Contenuto inappropriato + Profilo inappropriato + Solo il mittente e i moderatori lo vedono + Solo tu e i moderatori lo vedete + Spam + Segnala + Motivo della segnalazione? + La segnalazione verrà archiviata per te. + Segnala profilo: solo i moderatori del gruppo lo vedranno. + Segnala spam: solo i moderatori del gruppo lo vedranno. + Segnala violazione: solo i moderatori del gruppo lo vedranno. + moderatore + Archivia la segnalazione + Elimina la segnalazione + Segnalazioni + 1 segnalazione + segnalazione archiviata da %s + %d segnalazioni + Segnalazioni dei membri + Spam + Connessione bloccata + Il file è bloccato dall\'operatore del server:\n%1$s. + La connessione è bloccata dall\'operatore del server:\n%1$s. + Il contenuto viola le condizioni di utilizzo + Aprire il link? + + Apri i link dall\'elenco delle chat + No + Apri link + Chiedi + Imposta il nome della chat… + Disattiva eliminazione messaggi + 1 anno + Disattivare l\'eliminazione automatica dei messaggi? + Cambiare l\'eliminazione automatica dei messaggi? + Questa azione non è reversibile: i messaggi inviati e ricevuti in questa chat prima della selezione verranno eliminati. + Elimina i messaggi di chat dal tuo dispositivo. + I messaggi in questa chat non verranno mai eliminati. + predefinito (%s) + Usa la porta TCP %1$s quando nessuna porta è specificata. + Porta TCP per i messaggi + Usa porta web + Silenzia tutto + Menzioni non lette + Puoi menzionare fino a %1$s membri per messaggio! + I membri possono segnalare messaggi ai moderatori. + Archiviare tutte le segnalazioni? + Archiviare %d segnalazioni? + Archivia segnalazioni + Per tutti i moderatori + Per me + Segnalazione: %s + In questo gruppo è vietato segnalare messaggi. + Consenti di segnalare messaggi ai moderatori. + Tutte le segnalazioni verranno archiviate per te. + Vieta di segnalare messaggi ai moderatori. + Non perdere messaggi importanti. + Eliminazione dei gruppi più veloce. + Ricevi una notifica quando menzionato. + Aiuta gli amministratori a moderare i loro gruppi. + Menziona i membri 👋 + Privacy e sicurezza migliori + Invio dei messaggi più veloce. + Prestazioni dei gruppi migliorate + Organizza le chat in elenchi + Imposta la scadenza dei messaggi nelle chat. + Invia segnalazioni private + Nomi privati dei file multimediali. + rifiutato + rifiutato + Errore di lettura della password del database + in attesa di approvazione + in attesa + Condizioni aggiornate + 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. + 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. + I membri verranno rimossi dalla chat, non è reversibile! + I membri verranno rimossi dal gruppo, non è reversibile! + Rimuovere i membri? + I messaggi di questi membri verranno mostrati! + Sbloccare i membri per tutti? + Bloccare i membri per tutti? + moderatori + Tutti i nuovi messaggi di questi membri verranno nascosti! + Usando SimpleX Chat accetti di:\n- inviare solo contenuto legale nei gruppi pubblici.\n- rispettare gli altri utenti - niente spam. + Le chat private, i gruppi e i tuoi contatti non sono accessibili agli operatori dei server. + Accetta + Configura gli operatori dei server + Informativa sulla privacy e condizioni d\'uso. + Questo link richiede una versione più recente dell\'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile. + Link completo + Link breve + Link del canale SimpleX + Link di connessione non supportato + Usa link brevi (BETA) + Tutti i server + Off + Server preimpostati + Usa la porta TCP 443 solo per i server preimpostati. + 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 7899c374b2..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 @@ כל ההודעות יימחקו – לא ניתן לבטל זאת! ההודעות יימחקו רק עבורך. אשר זהות נסתרת הוסף שרתים מוגדרים מראש - הוסף שרת… + הוסף שרת לגשת לשרתים דרך פרוקסי SOCKS בפורט %d\? הפרוקסי חייב לפעול לפני הפעלת אפשרות זו. הגדרות רשת מתקדמות מראה @@ -60,8 +60,7 @@ קוד גישה לאפליקציה גרסת האפליקציה לכל פרופיל צ׳אט שיש ברשותך באפליקציה.]]> - חיבור TCP נפרד (ואישור SOCKS) ייווצר לכל איש קשר וחבר קבוצה. -\nשימו לב: אם ברשותכם חיבורים רבים, צריכת הסוללה ותעבורת האינטרנט עשויה להיות גבוהה משמעותית וחלק מהחיבורים עלולים להיכשל. + לכל איש קשר וחבר קבוצה. \nשימו לב: אם ברשותכם חיבורים רבים, צריכת הסוללה ותעבורת האינטרנט עשויה להיות גבוהה משמעותית וחלק מהחיבורים עלולים להיכשל.]]> הנמען התבקש לקבל את הסרטון הנמען התבקש לקבל את התמונה צרף @@ -165,8 +164,6 @@ מסד הנתונים של הצ׳אט נמחק ‬מסד הנתונים של הצ׳אט יובא אשר שדרוגי מסד נתונים - ארכיון צ׳אט - ארכיון צ׳אט צ׳אט מופסק לא ניתן להזמין את אנשי הקשר! שונה תפקידך ל%s @@ -222,7 +219,6 @@ צור צור פרופיל יצירת הפרופיל שלך - נוצר ב־%1$s צור קישור קבוצה צור קישור יוצר הקבוצה @@ -253,7 +249,6 @@ צרו כתובת כדי לאפשר לאנשים להתחבר אליכם. מבוזר מסד הנתונים מוצפן באמצעות סיסמה אקראית. אנא שנו אותה לפני הייצוא. - למחוק ארכיון צ׳אט\? מחק פרופיל צ׳אט ברירת מחדל (%s) %d יום @@ -307,7 +302,6 @@ סיסמה וייצוא של מסד הנתונים מחק אחרי מחק את כל הקבצים - מחק ארכיון מחק עבורי מחק קישור למחוק פרופיל צ׳אט\? @@ -377,7 +371,7 @@ מופעל מופעל עבור איש הקשר מופעל עבורך - הודעות נעלמות אסורות בקבוצה זו. + הודעות נעלמות אסורות. %d דקה %d שנ׳ הודעות נעלמות @@ -388,7 +382,7 @@ %d שבועות שמות שונים, אווטארים ובידוד תעבורה. ישיר - הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו. + הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו. הזן את שמך: שם תצוגה אינו יכול להכיל רווחים. %d חודשים @@ -495,10 +489,10 @@ הקבוצה תימחק עבורך – לא ניתן לבטל זאת! הסתר העדפות קבוצה - חברי קבוצה יכולים למחוק הודעות שנשלחו באופן בלתי הפיך. (24 שעות) - חברי הקבוצה יכולים לשלוח הודעות נעלמות. - חברי הקבוצה יכולים לשלוח הודעות ישירות. - חברי הקבוצה יכולים לשלוח הודעות קוליות. + משתמשים יכולים למחוק הודעות שנשלחו באופן בלתי הפיך. (24 שעות) + משתמשים יכולים לשלוח הודעות נעלמות. + משתמשים יכולים לשלוח הודעות ישירות. + יכולים לשלוח הודעות קוליות. אפשר השמדה עצמית אם תבחרו לדחות השולח לא יקבל התראה על כך. אם תאשרו, שרתי העברת ההודעות יוכלו לראות את ה־IP שלכם, וספק האינטרנט שלכם – את השרתים אליהם אתם מחוברים. @@ -511,7 +505,7 @@ התעלם מיד ייבא מסד נתונים - חסין מפני ספאם ושימוש לרעה + חסין מפני ספאם לייבא מסד נתונים של צ׳אט\? תמונה נשלחה התמונה תתקבל כאשר איש הקשר יסיים להעלות אותה. @@ -527,10 +521,10 @@ זהות נסתרת באמצעות קישור קבוצה זהות נסתרת באמצעות קישור חד־פעמי קישור חיבור לא תקין - אפשרו ל-SimpleX לפעול ברקע בתיבת הדו-שיח הבאה. אחרת, ההתראות יושבתו.]]> + אפשר זאת בתיבת הדו-שיח הבאה כדי לקבל התראות על הודעות חדשות באופן מיידי.]]> התראות מיידיות מושבתות! הזמן חברי קבוצה - הוזמן + הזמין את עקיף (%1$s) מצב זהות נסתרת מגן על הפרטיות שלך על ידי שימוש בפרופיל אקראי חדש עבור כל איש קשר. גרסת מסד נתונים לא תואמת @@ -562,14 +556,14 @@ הזמן לקבוצה הזמן חברי קבוצה מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה. - מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו. + מחיקת הודעות בלתי הפיכה אסורה. להצטרף בתור %s זה מאפשר חיבורים אנונימיים רבים ללא שום נתונים משותפים ביניהם בפרופיל צ׳אט יחיד. זה יכול לקרות כאשר: \n1. פג תוקפן של ההודעות בלקוח השולח לאחר 2 ימים או בשרת לאחר 30 ימים. \n2. פיענוח הצפנת הודעה נכשל, מכיוון שאתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים. \n3. החיבור נפגע. - ניתן לשנות זאת מאוחר יותר באמצעות ההגדרות. + איך זה משפיע על הסוללה זה יכול לקרות כאשר אתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים. להצטרף לקבוצה\? הצטרף @@ -592,7 +586,6 @@ למדו עוד עזרה במרקדאון בואו נדבר ב־Simplex Chat - אם ל־SimpleX אין מזהי משתמש, איך ניתן להעביר הודעות\?]]> שגיאת Keychain הצטרף עם זהות נסתרת לעזוב קבוצה\? @@ -604,8 +597,6 @@ בהיר ודאו שלקובץ יש תחביר YAML תקין. ייצאו ערכת נושא כדי לקבל דוגמה למבנה תקין של קובץ ערכת נושא. ככל הנראה איש קשר זה מחק את החיבור איתך. - ייעשה שימוש במארחי Onion כאשר יהיו זמינים. - מארחי Onion יידרשו לחיבור. נחסם על ידי %s אין קוד גישה לאפליקציה ניתן לשלוח רק 10 סרטונים בו־זמנית @@ -617,7 +608,7 @@ קישור הזמנה חד־פעמי מרקדאון בהודעות רשת ושרתים - הגדרות רשת + הגדרות מתקדמות ארכיון מסד נתונים חדש הודעות חבר קבוצה @@ -674,7 +665,6 @@ יידרשו מארחי onion לחיבור. \nשימו לב: לא תוכלו להתחבר לשרתים ללא כתובת .onion. לא ייעשה שימוש במארחי Onion. - לא ייעשה שימוש במארחי Onion. שיחה שלא נענתה הצפנה מקצה־לקצה דו־שכבתית.]]> ללא הצפנה מקצה־לקצה @@ -700,7 +690,7 @@ תגובות אמוג׳י להודעות אסורות בקבוצה זו. אפשר לאנשי הקשר להוסיף תגובות אמוג׳י להודעות. אפשר תגובות אמוג׳י להודעות רק אם איש הקשר מאפשר אותן. - חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות. + משתמשים יכולים להוסיף תגובות אמוג׳י להודעות. רק אתם יכולים להוסיף תגובות אמוג׳י להודעות. רק איש הקשר שלכם יכול להוסיף תגובות אמוג׳י להודעות. פתח @@ -747,7 +737,7 @@ שרת מוגדר מראש פרטיות מוגדרת מחדש אנשים יכולים להתחבר אליכם רק דרך הקישורים שאתם משתפים. - פרוטוקול וקוד פתוחים – כל אחד יכול להריץ את השרתים. + כל אחד יכול לארח שרתים. תקופתי נא להזין את הסיסמה הקודמת לאחר שחזור גיבוי מסד הנתונים, לא ניתן לבטל פעולה זו. לאסור מחיקה בלתי הפיכה של הודעות. @@ -756,7 +746,7 @@ אנא בידקו את חיבור האינטרנט שלכם עם %1$s ונסו שוב. ייתכן שטביעת האצבע של התעודה בכתובת השרת שגויה פתיחת מסוף צ׳אט - פתיחת פרופילי צ׳אט + שנה פרופילי צ׳אט ממתין כתובת שרת מוגדר מראש סיסמה להצגה @@ -800,7 +790,6 @@ דחיה מדריך למשתמש.]]> דרגו את האפליקציה - קראו עוד ב־GitHub repository שלנו. GitHub repository שלנו.]]> יבוצע שימוש בשרת ממסר רק במידת הצורך. גורם אחר יכול לצפות בכתובת ה־IP שלך. שרת ממסר מגן על כתובת ה־IP שלך, אך הוא יכול לראות את משך השיחה. @@ -896,14 +885,11 @@ שחזור גיבוי מסד נתונים לשחזר גיבוי מסד נתונים\? שמור סיסמה ופתח את הצ׳אט - שמור ארכיון בחירת אנשי קשר - שמור צבע קוד גישה להשמדה עצמית שניות אישור תפקיד - ביטול שמור ועדכן את פרופיל הקבוצה לשמור הודעת פתיחה\? %s (נוכחי) @@ -995,7 +981,6 @@ לעצור צ׳אט\? עיצרו את הצ׳אט כדי לייצא, לייבא או למחוק את מסד הנתונים. לא תוכלו לקבל ולשלוח הודעות בזמן שהצ׳אט מופסק. עצור - עיצרו את הצ׳אט כדי לאפשר פעולות מסד נתונים. דלג על הזמנת חברים שתף כתובת SimpleX @@ -1020,7 +1005,7 @@ הקישו כדי להצטרף תום זמן חיבור TCP האפליקציה בודקת הודעות חדשות מעת לעת - היא משתמשת בכמה אחוזים מהסוללה ביום. האפליקציה לא משתמשת בהתראות דחיפה - נתונים מהמכשיר שלך לא נשלחים לשרתים. - התפקיד ישתנה ל־\"%s\". כל חברי הקבוצה יקבלו הודעה על כך. + התפקיד ישתנה ל־"%s". כל חברי הקבוצה יקבלו הודעה על כך. כדי להתחבר באמצעות קישור פלטפורמת ההודעות והיישומים המגנה על הפרטיות והאבטחה שלך. המזהה של ההודעה הבאה שגוי (קטן או שווה להודעה הקודמת). @@ -1031,8 +1016,8 @@ יותר מדי תמונות! תודה שהתקנתם את SimpleX Chat! קישור זה אינו קישור חיבור תקין! - צבעי ערכת נושא - התפקיד ישתנה ל־\"%s\". החבר יקבל הזמנה חדשה. + צבעי ממשק + התפקיד ישתנה ל־"%s". החבר יקבל הזמנה חדשה. השרתים לחיבורים חדשים של פרופיל הצ׳אט הנוכחי שלך הפלטפורמה הראשונה ללא כל מזהי משתמש - פרטית בעיצובה. הדור הבא של תקשורת פרטית @@ -1069,7 +1054,7 @@ שדרג ופתח צ׳אט כדי להגן על אזור הזמן, קובצי תמונה/קול משתמשים ב־UTC. העלה קובץ - שירות רקע SimpleX – הוא משתמש בכמה אחוזים מהסוללה ביום.]]> + SimpleX רץ ברקע במקום להשתמש בpush notifications.]]> כדי לקבל התראות, יש להזין את סיסמת מסד הנתונים בטל נעילה שליחה לא מורשית @@ -1078,7 +1063,6 @@ (כדי לשתף עם איש הקשר שלך) כדי להתחיל צ׳אט חדש השתמש עבור חיבורים חדשים - לעדכן הגדרות מארחי ‪.onion‬\?‬ כדי לאמת הצפנה מקצה־לקצה עם איש הקשר שלכם, יש להשוות (או לסרוק) את הקוד במכשירים שלכם. פרופילי צ׳אט לעדכן מצב בידוד תעבורה\? @@ -1093,7 +1077,7 @@ שימוש בצ׳אט עדכן שגיאת מסד נתונים לא ידועה: %s - פרופיל קבוצה עודכן + עידכן את פרופיל הקבוצה עדכן בטל הסתרת פרופיל צ׳אט בטל הסתרת פרופיל @@ -1145,7 +1129,6 @@ עליכם לאפשר לאיש הקשר שלכם לשלוח הודעות קוליות כדי שתוכלו לשלוח אותן. סרטון להתחבר למפתחי SimpleX Chat כדי לשאול כל שאלה ולקבל עדכונים.]]> - לקבל את ההודעות, אנשי הקשר שלכם – השרתים דרכם אתם שולחים להם הודעות.]]> שרתי WebRTC ICE %1$d הודעות שדולגו שבועות @@ -1256,15 +1239,15 @@ כיבוי אפליקציה אפשר לשלוח קבצים ומדיה. - מועדף + הוסף למועדפים קבצים ומדיה אין צ\'אטים מסוננים לכבות\? קבצים ומדיה אסורים! - קבצים ומדיה אסורים בקבוצה זו. - חברי הקבוצה יכולים לשלוח קבצים ומדיה. + קבצים ומדיה אסורים. + משתמשים יכולים לשלוח קבצים ומדיה. איתחול - שנוא + הסר מהמועדפים כבוי קו חוצה לאסור שליחת קבצים ומדיה. @@ -1545,8 +1528,8 @@ הצג קריאות API איטיות אפשרויות למפתח צור פרופיל - ו %d שאר האירועים - הגדר כתובת איש קשר חדש + בנוסף ל- %d אירועים אחרים + איש הקשר הגדיר כתובת חדשה לחץ לחיבור דפדפן האינטרנט המוגדר כברירת מחדל נדרש לשיחות. אנא הגדר דפדפן ברירת מחדל במערכת, ושתף מידע נוסף עם המפתחים. השיחה הזו מוגנת באמצעות הצפנה קצה-אל-קצה. @@ -1626,7 +1609,7 @@ החיבור עצר נתיב קובץ לא חוקי שיתפת נתיב קובץ לא חוקי. דווח על הבעיה למפתחי האפליקציה. - %1$d הודעות שנערכו על ידי %2$s + %1$d הודעות נחסמו על ידי %2$s %d הודעות סומנו כנמחקות האם לחזור על בקשת החיבור? חסום @@ -1649,13 +1632,13 @@ טעינה של הקובץ שימוש ממחשב שולחני חסומים %s - מחק איש קשר + איש קשר נמחק %d אירועי קבוצה %s, %s ו-%d חברים איש הקשר %1$s השתנה ל-%2$s כתובת איש קשר הוסרה תמונת פרופיל הוסרה - הגדר תמונת פרופיל חדשה + הגדיר תמונת פרופיל חדשה עדכן פרופיל מצב לא ידוע נוצר ב @@ -1716,4 +1699,396 @@ %1$s.]]> הסר משתתף הסתיים פסק הזמן הקצוב להתחברות למחשב השולחני - \ No newline at end of file + חיבור רשת יותר אמין. + מתי שהIP מוסתר + השתמש במסלול פרטי עם שרתים לא ידועים + אזהרה על אופן שליחת ההודעה + הראה סטטוס הודעה + מסלול פרטי להודעה 🚀 + סטטוס הודעה:%s + אנא וודא שהמכשיר והמחשב מחוברים לאותה רשת מקומית, ושהפיירוול של המחשב מאפשר את החיבור. +\nאנא תשתף כל בעיות אחרות עם המפתחים. + "אשר קבלת קבצים משרתים לא מוכרים" + שגיאת רשת - ההודעה פגה תוקף לאחר ריבוי ניסיונות שליחה + שגיאה:%1$s + כל החברים + הודעת המקור נשארת פרטית + רינגטון ל שיחה נכנסת + קובץ לא נמצא - ככל הנראה הקובץ נמחק או בוטל + שגיאת שרת קבצים:%1$s + הורדה + לא + ערכת נושא חדשה לצא\'ט + מרובע, עיגול, או כל דבר ביניהם + העבר ושמור הודעות + מתי שמתחבר שחיות קוליות ווידאו. + לא ניתן לשלוח הודעה + הודעות קוליות לא מאופשרות + שגיאת קובץ זמני + קבצים ומדיה לא מאופשרים + שגיאת קובץ + השתמש במסלול פרטי עם שרתים לא ידועים מתי שכתובת הIP לא מוגנת. + שלח הודעות ישירות מתי שכתובת הIP מוגנת ואתה או שרת היעד לא תומך במסלול פרטי. + כדי להגן על כתובת הIP שלך, מסלול פרטי משתמש בשרתי הSMP שלך כדי להעביר את ההודעות. + אל תשתמש במסלול פרטי + תמיד השתמש בנתיב פרטי + תאפשר בהגדרות + תאפשר הרשאות בשביל להתקשר + מצלמה ומיקרופון + פתח הגדרות + מצא את ההרשאה בהגדרות המכשיר ותאפשר אותה ידנית + רמקול + אוזניות + אוזניות + ערכת נושא לפרופיל + צבעי הצא\'ט + הראה רשימת צא\'טים בחלון חדש + מצב הקובץ + סטטוס הודעה + מצב הקובץ:%s + ריק + כהה + ערכת נושא + שחור + בהיר + אפס צבע + ערכת נושא לאפליקציה + שלח תגובה + התקבל תגובה + טפט רקע + הסר תמונה + חזור + מלא + אפס ערכת נושא לאפליקציה + התאם + צהריים טובים + בוקר טוב! + הגדרות מתקדמות + הגדר ערכת נושא ברירת מחדל + אפס ערכת נושא למשתמש + החל ל + ערכת נושא + משתמשים יכולים לשלוח קישורי SimpleXצ + עשה שהצאט\'ים שלך יראו אחרת! + הגדרות רשת + הקישור הזה שומש כבר במכשיר אחר, אנא צור קישור חדש במחשב. + חיבור קווי + סלולרי + נשמר + נשמר מ%s + הועבר + הועבר + מחובר לרשת + Wi-Fi אלחוטי + שגיאה בהעתקה + ערכת נושא כהה + ערכת נושא + צבעי מצב כהה + שגיאה בשרת היעד:%1$s + "אל תשלח הודעות ישירות אפילו אם שרת היעד לא תומך במסלול פרטי" + שיפור בשליחת הודעות + מצלמה + אפשר שליחת קישורי SimpleX + בעלים + מנהלים + מופעל עבור + שרתים לא ידועים! + בלי טור או VPN, כתובת הIP שלך תהיה חשופה למתווכי XFTP האלה: +\n%1$s + הועבר מ + נשמר + נשמר מ + מקבלי ההודעה לא יוכלו לראות מי שלח את ההודעה + העבר + מעביר הודעה… + קישורי SimpleX לא מאופשרים + מסלול פרטי + לא מוגן + תמיד + מתווכים לא ידועים + לעולם לא + כן + אפשר שינמוך + שלח הודעות ישירות מתי שאתה או שרת היעד לא תומך במסלול פרטי + מיקרופון + הרשאות שניתנו + כתובת IP מוגנת + האפליקציה תשאל כדי לאשר הורדות משרתי קבצים לא ידועים (למעט שרתי טור או מתי שפרוקסי SOCKS מופעל). + בלי טור או VPN, כתובת הIP שלך תהיה גלויה לשרתי קבצים. + קבצים + תמונות פרופיל + מסלול הודעה פרטית + קישורי SimpleX + אין חיבור לרשת + אחר + ערכת נושא בהירה + מערכת + שגיאה בהצגת התראה, צור קשר עם המפתחים + שגיאה בהתחברות לשרת %1$s, אנא נסה מאוחר יותר + אין עדיין חיבור ישיר, ההודעה תעובר ע"י מנהל. + חבר לא פעיל + שרתי XFTP אחרים + הראה אחוזים + מושבת + יציבה + הותקן בהצלחה + התקן עדכון + פתח מיקום קובץ + אנא הפעל מחדש את האפילקציה. + הזכר מאוחר יותר + דלג על הגרסא הזאת + השבת + גדול פונט + פרופיל נוכחי + הודעות שנשלחו + פג תוקף + שגיאות בשליחה + גודל + קבצים שהועלו + יחול בצ\'אטים ישירים! + בלוטוס + טשטש מדיה + כבוי + בינוני + חזק + מושבת + לא פעיל + פרטים + שגיאה + שגיאה בהתחברות מחדש לשרת + שגיאה בהתחברות מחדש לשרתים + שגיאות + מקבל ההודעות + שרתי פרוקסי + התחבר מחדש לכל השרתים + להתחבר מחדש לשרת? + להתחבר מחדש לשרתים? + סטטיסטיקות + סך הכל + ניסיונות + הושלם + חיבורים + נמחק + שגיאות במחיקה + שגיאה באיפוס הסטטיסטיקה + אחר + מאובטח + שלח ישירות + נשלח דרך פרוקסי + קבצים שהורדו + שגיאות בהורדה + פתח הגדרות שרת + כתובת שרת + חריגה מהקיבולת - הנמען לא קיבל הודעות שנשלחו בעבר. + בדוק עבור עדכונים + מחובר + שרתים מחוברים + בודק עבור עדכונים חדשים + מתחבר + נוצר + הודעה הועברה + ההודעה תוכל להימסר מאוחר יותר אם החבר יהפוך לפעיל. + הודעות שהתקבלו + אפס את כל הסטטיסטיקות + גודל + קישורי SimpleX לא מאופשרים בקבוצה הזו. + סרוק/ הדבק קישור + אנא נסה מאוחר יותר + שרתי SMP אחרים + אפס + הועלה + סטטיסטיקה מפורטת + בטא + לאפס את כל הסטטיסטיקות? + שגיאות בפענוח + שגיאות אחרות + שגיאה בהעלאה + יורד עדכון לאפליקציה + מוריד עדכון לאפליקציה, אל תסגור את האפליקציה + כל הפרופילים + קבצים + אין מידע, נסה לרענן + מידע על השרתים + התקבל סה"כ + התקבלו שגיאות + התחבר מחדש + שלח הודעות + נשלח בסה"כ + שרת SMP + שרת XFTP + חלש + אנשי קשר בארכיון + דיווח בארכיון + הפרה של הנחיות קהילתיות + %1$d שגיאת קבצים:\n%2$s + %1$d הקבצים עדיין בהורדה. + הסכם לתנאים + %1$d ההורדה של הקובץ/ים עדיין לא הסתיימה. + התנאים המקובלים עלי + שנה + סיבה אחרת + כתובת עסקית + שיפור בסידור של הודעות לפי תאריכים. + שיפור ביצועים לקבוצות + התקשר + לא ניתן להתקשר לחבר קבוצה + %s.]]> + להעביר דיווח לארכיון? + מסד נתונים של הצא\'טים + בדוק עבור הודעות חדשות כל 10 דקות + אישרת את תנאי השימוש ב:%s. + הצ\'אט יימחק עבור כל החברים - לא ניתן לבטל את זה! + לחץ על כפתור מידע ליד שדה כתובת כדי לאפשר שימוש במיקרופון. + %s.]]> + %s.]]> + העבר אנשי קשר לארכיון לשוחח מאוחר יותר + לשנות את מחיקת ההודעה האוטומטית? + הצ\'אט כבר קיים! + הכל + %1$d שגיאה/ות קובץ אחר/ות. + %1$d הקובץ/ים נכשל/ו בהורדה. + כל ההודעות החדשות מחברים אלו יוסתרו! + לחסום את חברי הקבוצה לכולם? + הדיווח הועבר לארכיון ע\"י %s + עם איש קשר אחד בלבד - שתף באופן אישי או באמצעות כל מסנג\'ר.]]> + טשטוש בשביל שיפור הפרטיות. + דיווח 1 + כל הצ\'אטים יוסרו מהרשימת %s, והרשימה תימחק + שנה רשימה + לאפשר שיחות? + הצ\'אט יימחק עבורך - אי אפשר לבטל את זה! + שיחות לא מורשות! + שאל + %s.]]> + מכשירי שיואמי: אנא תאפשר הפעלה אוטומטית בהגדרות הטלפון שלך כדי שההתראות על הודעות חדשות יפעלו.]]> + %1$s ההודעות לא הועברו. + מוצפנים מקצה לקצה, עם אבטחה פוסט-קוונטית בהודעות ישירות.]]> + טישטוש + עסקי + %s, קבל את תנאי השימוש.]]> + כל הדיווחים אצלך יועברו לארכיון. + להעביר לארכיון %d דיווחים? + להעביר לארכיון את כל הדיווחים? + העבר דיווח לארכיון + דיווחים בארכיון + אפשר לדווח על הודעות למנהלים. + %1$d הקובץ/ים נמחקו. + %s.]]> + המסד נתונים יוצא בהצלחה + %1$s כבר באנשי קשר.]]> + לא ניתן לשלוח הודעה לחבר קבוצה + קבל הזמנה + על המפעילים + לא ניתן להתקשר לאיש קשר + שיפור לשיחות + שנה את הסדר + ארכיון + אני מסכים + שיפור בפרטיות ובאבטחה + אבטחה יותר טובה✅ + ממשק משתמש יותר נוח + "גרסאת שרת היעד %1$s אינה תואמת עם שרת ההעברה %2$s." + מחק עד 20 הודעות בבת אחת. + הודעות ישירות בין חברים חסומות. + השבת מחיקת הודעות + אל תשתמש בתעודות עם פרוקסי. + ירד + %d דיווחים + יורד %s (%s) + השבת מחיקת הודעות אוטומטית? + כפילויות + תנאי שימוש + מחק בלי להתריע + הורדה + הודעות ישירות בין חברים אסורות בצ\'אט זה. + אל תחמיץ הודעות חשובות. + הורד גרסאות חדשות מ GitHub. + פרופיל לא הולם + לפרטיות מטא דאטא טובה יותר. + אפשר Flux בהגדרות רשת ושרתים בשביל לשפר את הפרטיות של המטא דאטא + מעביר %1$s הודעות + שגיאה בקריאת משפט-סיסמה של מסד נתונים + משתמשים יכולים לדווח על הודעות לאחראי תוכן + שגיאת אתחול ב WebView, וודא שיש לך WebView מותקן והוא תותך בארכיטקטורה arm64\nשגיאה: %s + שגיאה בשמירת פרוקסי + לפרופיל צ\'אט %s: + למסלול פרטי + שגיאה בשמירת ההגדרות + שגיאה בהעברת ההודעות + שגיאה בהחלפת פרופיל + אפשר לוגים + איך זה משפר את הפרטיות + שיפור בגלילה בצ\'אט + תוכן לא הולם + שגיאה בקבלת תנאי שימוש + שגיאות בתצורת השרתים. + שרת ההעברות %1$s לא הצליח להתחבר לשרת היעד %2$s.נסה שוב במועד מאוחר יותר. + דיווחים מחברי הקבוצה + שרתי קבצים ומדיה + קישור לא תקין + שגיאה ביצירת רשימת צא\'טים חדשה + שגיאת עדכון רשימת צ\'אטים + שגיאה בטעינת רשימות הצא\'טים + ערוך + ודא שתצורת ה- proxy נכונה. + הזמן + זה מגן על כתובת ה- IP והחיבורים שלך. + עזוב צ\'אט + תקן + לתקן חיבור? + מחיקה מהירה יותר של קבוצות. + שליחת הודעות מהירה יותר. + העבר הודעות ללא קבצים? + הזמן לצ\'אט + שרת ההעברות: %1$s\nשגיאת שרת יעד: %2$s + שרת ההעברות: %1$s\nשגיאה: %2$s + למדיה חברתית + הגדל את גודל הגופן. + רשימה + מעודפים + קבוצות + שם הרשימה.. + שם הרשימה והאמוג\'י צריכים להיות שונים משאר הרשימות. + שמור שיחה + הזמן + ממשק בשפה הליטאית + הקובץ נחסם ע\"י מפעיל השרת:\n%1$s. + בשבילי + לכל האחראי תוכן + העבר הודעות… + שגיאה בשמירת המסד נתונים + שגיאה בהוספת שרת + שגיאת עדכון שם שרת + להעביר %1$s הודעה/ות? + שגיאה בשליחת הדיווח + שגיאה בשמירת שרתים + לעזוב את הצ\'אט? + העבר עד 20 הודעות בבת אחת. + קבל התראה כאשר מתייגים אותך + עזור לאחראי תוכן בדיווחים על תוכן בעייתי בקבוצות + הוסף רשימה + הדגשה נוספת 2 + בשימוש שלך ב- SimpleX Chat אתה מסכים ל:\n\n- לשלוח רק תוכן חוקי בקבוצות ציבוריות.\n\n- לכבד את שאר המשתמשים - לא לשלוח ספאם. + %s.]]> + %s.]]> + הוסף חברים + שרתי הודעות שנוספו + שרתי מדיה וקבצים שנוספו + הוסף לרשימה + חיבורים פעילים + הוסף את חברי הצוות שלך לשיחות. + האפליקציה תמיד רצה ברקע + הגדרות כתובת + סרגל הכלים של האפליקציה + סשן לאפליקציה + כתובת או קישור חד פעמי? + הוסף חברי צוות + %s.]]> + צ\'אט + צא\'טים עסקיים + "הזבל נמחק" + אנשי קשר + איש הקשר יימחק - לא ניתן לבטל זאת! + איש הקשר נמחק. + ספאם + 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 83d59b3705..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 @@ QRコードでサーバを追加 別の端末に追加 プロフィールを追加 - サーバを追加… + サーバを追加 SOCKSプロキシ(ポート%d)経由で接続しますか?(※設定する前にプロキシ起動が必要※) 全チャットとメッセージが削除されます(※元に戻せません※)! 送信相手からの音声メッセージを許可する。 あなたと連絡相手が音声メッセージを送信できます。 - 電池省エネに良い:バックグラウンド機能で10分毎に新着メッセージを確認します。通話と緊急メッセージを見逃す可能性があります。]]> + バッテリーに優しい。アプリは10分ごとにメッセージを確認します。ただし、電話や緊急のメッセージを見逃す可能性があります。]]> 音声オフ 添付する アプリ・ビルド番号: %s @@ -41,8 +41,7 @@ アプリのバージョン アプリのバージョン: v%s アプリ内の各チャットプロフィールに、.連絡先毎にそれぞれのTCP接続(とSOCKS資格情報)が使われます。]]> - 各連絡先とグループに、それぞれのTCP接続(とSOCKS資格情報)が使われます。 -\n※注意※ 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。 + 各連絡先とグループに、それぞれのTCP接続(とSOCKS資格情報)が使われます。 \n※注意※ 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。]]> 太文字 音声通話 音声とビデオ通話 @@ -77,8 +76,6 @@ 電池消費が最少:アプリがアクティブ時のみに通知が出ます(バックグラウンドサービス無し)。]]> 設定メニューにてオフにできます。 アプリがアクティブ時に通知が出ます。]]> あなたと連絡相手が送信済みメッセージを永久削除できます。(24時間) - チャットのアーカイブ - チャットのアーカイブを削除しますか? シークレットモードで参加 接続待ち (招待) 接続待ち (承諾済み) @@ -114,8 +111,8 @@ チャット読み込みに失敗 チャット読み込みに失敗 アプリを更新し、開発者にご連絡ください。 - 即時通知! - 即時通知が無効になってます! + 通知の常時受信! + 通知の常時受信が無効になってます! パスフレーズが必要 プライベート 連絡先の名前 @@ -153,13 +150,12 @@ グループのプロフィールはサーバではなく、メンバーの端末に保存されます。 グループのプロフィールが更新されました。 連絡先とメッセージ内容をプライベートにする。 - 即時通知 + 通知の常時受信 SMPサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 - if SimpleX にユーザIDがなければ、メッセージをどうやって届けるのでしょうかと。]]> SimpleX の仕様 通話中 - 電池消費がより高い!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信次第に通知がすぐに出ます)。]]> + 電池消費がより高い!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信してすぐに通知が出ます)。]]> 発信中 通話終了 %1$s 通話が終了しました。 @@ -204,7 +200,6 @@ 接続済み リンク経由で繋がる。 接続エラー - 接続にオニオンのホストが必要となります。 接続待ち (紹介済み) 接続エラー (AUTH) 接続タイムアウト @@ -222,7 +217,6 @@ 追加情報アイコン オニオンのホストが利用可能時に使われます。 オニオンのホストが使われません。 - オニオンのホストが利用可能時に使われます。 画像を1回で最大10枚を送信できます。 2層エンドツーエンド暗号化で送信されたプロフィール、連絡先、グループ、メッセージは、クライント端末にしか保存されません。]]> グループ設定を変えられるのはグループのオーナーだけです。 @@ -256,7 +250,7 @@ 招待が期限切れました! サーバを削除 端末認証がオフです。SimpleXロックを解除します。 - このグループではメンバー間のダイレクトメッセージが無効です。 + このグループではメンバー間のダイレクトメッセージが無効です。 このグループでは消えるメッセージが無効です。 %d 分 %d 週 @@ -298,7 +292,7 @@ ファイル保存でエラー発生 接続中 確認待ち - 中止 + キャンセル 使い捨ての招待リンク 音声メッセージを録音 ギャラリーから @@ -314,8 +308,7 @@ 自分のサーバの使い方 ICEサーバ (1行に1サーバ) ネットワークとサーバ - ネットワーク設定 - オニオンのホストが使われません。 + 高度な設定 アドレスを削除 保存せずに閉じる 表示の名前には空白が使用できません。 @@ -327,13 +320,12 @@ 接続中 発信中… 終了 - プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。 + 誰でもサーバーをホストできます。 プライバシーを再定義 技術の説明 プライベートな接続をする プライベートな通知 GitHubリポジトリで詳細をご確認ください。]]> - GitHubリポジトリで詳細をご確認ください。 エンドツーエンド暗号化済みビデオ通話 無効にする エンドツーエンド暗号化がありません @@ -372,8 +364,6 @@ データベース暗号化のパスフレーズが更新されます。 チャットを開くにはデータベースパスフレーズが必要です。 ファイル: %s - 作成日時 %1$s - アーカイブを削除 参加 グループに参加しますか? グループに参加 @@ -490,7 +480,7 @@ 新しい繋がりのリクエスト コピー メッセージを削除しますか? - 編集する + 編集 プライベートにする メッセージが削除されます (※元に戻せません※)! メッセージが削除対象となります。宛先にはメッセージの解読ができます。 @@ -538,9 +528,9 @@ 色付き 応答 分散型 - スパムや悪質送信を完全防止 - 即時 - 定期的 + スパム耐性 + 常時受信 + 定期的に受信 通話は既に終了してます! エンドツーエンド暗号化済みの音声通話 無視 @@ -554,7 +544,6 @@ 端末 送受信済みのファイルがありません メッセージを削除 - チャットのアーカイブ 接続中 あなたを除名しました。 グループのリンク @@ -673,7 +662,6 @@ スピーカーオフ あなたのチャットデータベース 停止 - データベース操作をするにはチャットを停止する必要があります。 SimpleX連絡先アドレス SimpleX使い捨て招待リンク 連絡先アドレスリンク経由 @@ -735,7 +723,6 @@ あなたのICEサーバ 直接にインタネットに繋がりますか? SOCKSプロキシを使いますか? - .onionのホスト設定を更新しますか? .onionホストを使う 利用可能時に トランスポート隔離 @@ -752,9 +739,8 @@ 取り消し線 接続中… 次世代のプライベートメッセンジャー - 受信サーバを決められます。あなたの連絡先が同じく、自分に対する受信サーバを決められます。]]> ビデオ通話 - アプリが稼働中に + アプリがアクティブ時のみ WebRTC ICEサーバ あなたのICEサーバ 設定 @@ -792,7 +778,6 @@ 保存 ネットワーク設定を更新しますか? 設定を更新すると、全サーバにクライントの再接続が行われます。 - 色を保存 あなたが次を許可しています: オン グループ設定を行う @@ -884,7 +869,6 @@ アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。 データベースのパスフレーズ変更が完了してません。 リンク、またはQRコードを共有できます。誰でもグループに参加できます。後で削除しても、グループのメンバーがそのままのこります。 - 元に戻す 更新 1日に設定 一定時間が経ったら送信されたメッセージが削除されます。 @@ -927,7 +911,6 @@ データベースのエクスポート、読み込み、削除するにはチャット機能を停止する必要があります。チャット機能を停止すると送受信ができなくなります。 あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。 データベースパスフレーズを更新 - アーカイブを保存 あなたが自分の役割を次に変えました:%s アドレスを変えました %sのアドレスを変えました @@ -1035,7 +1018,7 @@ メンバーのメッセージを削除しますか? テーマのインポート ポート - テーマカラー + インターフェースカラー 共有を停止 友人を招待する 後からでも作成できます @@ -1085,7 +1068,7 @@ Bluetoothのサポートおよびその他の機能も改善されました。 中継サーバーは必要な場合にのみ使用されます。 別の当事者があなたの IP アドレスを監視できます。 アドレス共有の停止? - 送信 + 送信メッセージ 受信したメッセージ 以前のメッセージとハッシュ値が異なります。 次のメッセージの ID が正しくありません (前のメッセージより小さいか等しい)。 @@ -1284,7 +1267,7 @@ KB あたりのプロトコル タイムアウト グループメンバーはファイルやメディアを送信できます。 アドレス変更の中止 - このグループでは、ファイルとメディアは禁止されています。 + ファイルとメディアは禁止されています。 終了しますか? アプリを再起動するまで通知は機能しません。 未読とお気に入りをフィルターします。 @@ -1595,8 +1578,8 @@ ステータス不明 プライベートノート メッセージ配信の改善 - エンドツーエンドの暗号化によって保護されます。]]> - 耐量子E2E暗号化によって保護されます。]]> + エンドツーエンドの暗号化によって保護されます。]]> + 耐量子E2E暗号化によって保護されます。]]> このチャットはエンドツーエンド暗号化により保護されています。 このチャットは耐量子エンドツーエンド暗号化により保護されています。 プライベートノート @@ -1769,4 +1752,260 @@ プロフィール画像 正方形、円形またはその中間 プロフィール画像をシェイプ - \ No newline at end of file + 宛先サーバエラー: %1$s + エラー: %1$s + プライベートルーティング + 不明なリレー + 未保護 + ダウングレードを許可 + 常時 + 常時プライベートルーティングを使用 + プライベートメッセージルーティング + メッセージステータスを表示 + システム + ブラック + 色設定 + ダーク + ダークモードカラー + ライト + アプリのテーマ + ダークモード + ライトモード + 適用先 + 追加のアクセント2 + 高度な設定 + こんにちは! + おはよう! + 壁紙のアクセント + 壁紙の背景 + チャットカラー + チャットテーマ + ファイルエラー + ファイルサーバーエラー: %1$s + ファイルステータス + ファイルステータス: %s + ペルシャ語UI + ファイルが見つかりません - 削除されたかキャンセルされた可能性があります。 + チャットの見た目を変更できます! + ファイル + ネットワークエラー - 複数回送信が試行されましたが、メッセージが期限切れになりました + 無効 + メッセージステータス + ファイルの安全な受け取り + プロフィールテーマ + メッセージステータス:%s + 色のリセット + 受信した返信元メッセージ + 送信した返信元メッセージ + 画像を削除 + 拡大縮小 + フィットさせる + 画像全体 + アプリのテーマをリセット + ユーザーテーマをリセット + デフォルトのテーマを設定 + プライベートメッセージルーティング 🚀 + 不明なサーバーからのファイルを確認できます + バッテリーの使用量が減少しました + メッセージ配信の改善 + 不明なサーバーです! + プライベートルーティングを使用しない + IPアドレス保護 + コピーエラー + 新しいチャットテーマ + 連絡先が選択したメッセージリレーからあなたのIPアドレスを保護します。 +\n*ネットワークとサーバー*設定から有効にして下さい。 + いいえ + はい + メッセージルーティングモード + フォントサイズ + ベータ + アップデートを確認 + アップデートを確認 + 完了 + SMPサーバーの構成 + 接続中 + XFTPサーバーの構成 + チャトリスト切り替え + 連絡先 + メッセージサーバ + メディア&ファイルサーバ + チャットツールバーを近づける + 招待 + 作成 + メッセージ + チャットツールバーを近づける + QRスキャン / リンクの貼り付け + 片手でアプリを利用できます + 招待 + リンクの貼り付け + 無効 + 現在のプロフィール + 全てのプロフィール + 通話 + 連絡先の削除を確認しますか? + 接続 + 連絡先が削除されます - この操作は取り消せません! + プロフィールの切り替えエラー + メディアのぼかし + チャットデータベース + 続ける + 連絡先の削除完了! + 詳細 + 非アクティブ + 無効 + プロキシの保存エラー + 通話を許可しますか? + 連絡先が削除されました。 + 無効 + 一度に最大20件のメッセージを削除できます。 + 接続中のサーバ + エラー + サーバーへの再接続エラー + エラー + ファイル + 復号化エラー + 削除エラー + 重複 + 期限切れ + 統計の詳細 + プロキシで認証情報を使用しないでください。 + サーバーへの再接続エラー + 統計のリセットエラー + メッセージを送信することができません + 連絡先と通話することができません + 接続待ち + 無し + 控え目 + 保存して再接続 + 強め + 普通 + パーセンテージを表示 + これは見た目の設定から変更できます。 + 以前接続していたサーバ + アクティブな接続 + 統計情報 + アーカイブされた連絡先 + 角丸 + 送信されたメッセージ数 + 全ての統計情報をリセットしますか? + 合計 + しっぽ + サーバ情報 + %sから計測されています。\nデバイス上の全てのデータはプライベートです。 + 受信したメッセージ数 + サーバの統計情報をリセットしようとしています - これは元に戻せません! + 全統計情報をリセットする + ヒントをリセットする + 容量を超えました - 受信者は以前に送信されたメッセージを受け取っていません。 + 試行 + 確認 + 確認エラー + 削除完了 + 作成完了 + %1$d件のその他のファイルエラー。 + メッセージの転送エラー + %1$d件のファイルエラー:\n%2$s + %1$s件のメッセージを転送しますか? + %1$d件のファイルがまだダウンロード中です。 + %1$d件のファイルがダウンロードされませんでした。 + %1$d件のファイルのダウンロードに失敗しました。 + %1$d件のファイルが削除されました。 + %1$s件のメッセージが転送されませんでした。 + ダウンロード + %1$s件のメッセージを転送中 + 会話が削除されました! + アプリのアップデートがダウンロードされました + アプリの更新をダウンロード中です。アプリを閉じないでください + %s(%s)をダウンロード + グループメンバーにメッセージを送信できません。 + 後でチャットするために連絡先をアーカイブします。 + 接続とサーバーのステータス + アドレスフィールドの近くにある情報ボタンをクリックして、マイクの使用を許可してください。 + 接続 + カスタマイズ可能なメッセージの形。 + プライベートルーティングをサポートしていなくても、メッセージを直接送信しないでください。 + 転送サーバー%1$sへの接続エラーです。後ほど再試行してください。 + WebViewの初期化エラーです。WebViewがインストールされており、サポートされているアーキテクチャがarm64であることを確認してください。\nエラー:%s + WebViewの初期化エラーです。システムを新しいバージョンに更新してください。開発者にお問い合わせください。\nエラー:%s + %1$sの宛先サーバーアドレスは、転送サーバー%2$sの設定と互換性がありません。 + %1$sの宛先サーバーバージョンは、転送サーバー%2$sと互換性がありません。 + 通知なしで削除 + チャンクが削除されました + すべてのカラーモード + グループメンバーに電話できません + 連絡先に接続中です。しばらくお待ちいただくか、後で確認してください! + ネットワークを管理 + ダウンロードしたファイル + メンバーの%d件のメッセージを削除しますか? + チャットデータベースがエクスポートされました + 通話禁止! + チャンクがダウンロードされました + チャンクがアップロードされました + ダウンロードエラー + アプリセッション + 配信のデバッグ + 改善された通話機能 + メッセージの日付 + より強力なセキュリティ ✅ + より良いユーザー体験 + 最大200件のメッセージを削除または管理します。 + プライバシー向上のためのぼかし処理。 + 友達ともっと速くつながりましょう。 + 新しいバージョンをGitHubからダウンロードしてください。 + ダウンロード完了 + 再接続 + 送信エラー + SOCKSプロキシ + パスワード + 設定 + 情報がありません、リロードしてください + SMPサーバ + メッセージ + ウェブサイト + ビデオ + 設定画面からサーバを構成できます。 + 改定履歴を開く + 全てのチャットが %s から削除され、リスト自体も削除されます + アプリのツールバー + ログを有効化 + メッセージ受信を10分毎に確認します + バックグラウンドでアプリが常時動作します + SimpleX ChatとFluxは、Fluxが運営するサーバをアプリに組み込むことに合意しました。 + 利用条件をレビュー + プリセットサーバ + 利用条件を承諾 + 透過度 + %s の利用条件が受け入れられます。]]> + 新しいメッセージ + メッセージの形 + リストを保存 + 削除 + リスト名... + 編集 + リストを削除しますか? + 利用条件 + 続ける + リストを作成 + サーバオペレータ + 利用条件を開く + バックグラウンドサービスを使用しない + 利用条件の承諾 + %s の利用条件に承諾しています。]]> + 後で作成する場合はメニューから「SimpleXのアドレスを作成」を選択してください。 + 運営者について + 1年 + 1 件のレポート + モデレーターにメッセージを報告することを許可する + これらのメンバーからの新しいメッセージはすべて非表示になります! + リストに追加 + アドレス設定 + チームのメンバーを会話に追加する + 追加されたメッセージサーバー + アドレスか使い捨てのリンク? + 友達を追加 + チームメンバーを追加 + リストを追加 + すべて + ワンタイムリンクを生成 + 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 af3972035f..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 @@ 연결됨 연결 중 - 그룹 링크를 통해 연결하시겠습니까\? - 초대 링크로 연결하시겠습니까\? + 그룹에 참여할까요? + 일회용 링크로 연결하시겠습니까? 연결 수립됨 연결 시간 초과 파일을 받을 수 없음 @@ -12,7 +12,7 @@ 연결 오류(인증) 대기열 만들기 데이터베이스를 초기화할 수 없음 - 백그라운드 서비스가 항상 실행 됩니다. - 메시지를 받는 즉시 알림이 표시됩니다. + 백그라운드 서비스가 항상 실행됨 – 메시지를 받는 즉시 알림이 표시됩니다. 10분마다 최대 1분간 새 메시지 확인 연결됨 숨긴 대화 상대: @@ -29,13 +29,13 @@ 뒤로 취소 라이브 메시지 취소 - 파일 선택 + 파일 확인 링크 / QR 코드로 연결 클립보드로 복사됨 비밀 그룹 생성 수락 - 모든 메시지가 삭제됩니다 - 삭제 후 되돌릴 수 없습니다! 메시지는 나에게서만 삭제됩니다. + 모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 메시지는 나에게서만 삭제됩니다. 지우기 지우기 채팅 지우기 @@ -43,7 +43,7 @@ 연결 요청이 전송되었습니다! 링크로 연결 프리셋 서버 추가 - 서버 추가… + 서버 추가 채팅 콘솔 서버 주소를 확인 후 다시 시도해 주세요. ICE 서버 설정 @@ -56,7 +56,7 @@ 앱 버전 앱 버전 : v%s 코어 버전 : v%s - 전화 응답 + 통화 응답 굵게 전화 연결 중 색깔 @@ -68,13 +68,13 @@ 연결됨 연결 중… 내 프로필 생성 - 배터리에 좋음. 백그라운드 서비스는 10분마다 메시지를 확인합니다. 전화나 긴급 메시지를 놓칠 수 있습니다.]]> + 배터리에 좋음. 앱이 10분마다 메시지를 확인합니다. 전화나 긴급 메시지를 놓칠 수 있습니다.]]> 통화가 이미 종료되었습니다! 항상 릴레이 사용 음성 통화 음성 & 영상 통화 잠금 화면에서의 통화: - 대화 상대와 종단간 암호화되지 않음 + 대화 상대와 종단 간 암호화되지 않음 응답 소리 켜기 소리 끄기 @@ -87,10 +87,9 @@ 채팅이 작동 중 채팅 채팅 데이터베이스를 가져옴 - 주의: 비밀구절(passphrase)을 분실하면 복구하거나 비밀번호 변경을 할 수 없어요.]]> - 데이터베이스 암호구절(passphrase)을 바꾸시겠습니까\? - 새로운 암호구절(passphrase) 확인… - 채팅 기록 보관함 + 주의: 암호를 분실하면 복구하거나 비밀번호 변경을 할 수 없어요.]]> + 데이터베이스 암호를 바꾸시겠습니까? + 새로운 암호 확인… 내 역할이 %s 역할로 변경됨 주소 바꾸는 중… 주소 바꾸는 중… @@ -115,13 +114,13 @@ 대화 상대가 허용함 연락처 개별 설정 대화 상대가 허용한 경우에만 음성 메시지를 보낼 수 있습니다. - 대화 상대가 전송한 메시지 영구 삭제를 허용합니다. + 대화 상대가 전송한 메시지 영구 삭제를 허용합니다. (24 시간) 대화 상대가 사라지는 메시지를 전송할 수 있도록 허용합니다. 대화 상대의 음성 메시지 전송을 허용합니다. - 당신과 대화 상대 모두 메시지를 영구 삭제할 수 있습니다. + 당신과 대화 상대 모두 메시지를 영구 삭제할 수 있습니다. (24 시간) 당신과 대화 상대 모두 음성 메시지를 보낼 수 있습니다. 상대가 메시지에 삭제 표시를 할 수 있습니다. 그러나 삭제 표시된 메시지 내용은 여전히 볼 수 있습니다. - 보낸 메시지 영구 삭제를 허용합니다. + 보낸 메시지 영구 삭제를 허용합니다. (24 시간) %s 취소됨 대화 요청 자동 수락 채팅 프로필(기본값) 또는 연결(베타). @@ -142,35 +141,34 @@ QR 코드 스캔으로 서버 추가 환영 메시지 추가 관리자 - 관리자는 그룹 가입을 위한 링크를 만들 수 있습니다. + 관리자는 그룹 참여 링크를 만들 수 있습니다. 사라지는 메시지를 보낼 수 있습니다. - 모든 채팅과 메시지가 삭제됩니다 - 되돌릴 수 없습니다! + 모든 채팅과 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 음성 메시지 전송을 허용합니다. 음성 메시지를 허용하시겠습니까\? 대화상대가 허용하는 경우에만 사라지는 메시지를 허용합니다. - 그룹 구성원에게 다이렉트 메시지 보내는 것을 허용합니다. - 모든 그룹 구성원이 연결된 상태로 유지됩니다. - 대화 상대가 허용하는 경우에만 영구적인 메시지 삭제를 허용합니다. + 그룹 멤버에게 다이렉트 메시지 보내는 것을 허용합니다. + 모든 그룹 멤버가 연결된 상태로 유지됩니다. + 대화 상대가 허용하는 경우에만 영구적인 메시지 삭제를 허용합니다. (24 시간) 모든 대화 상대가 연결된 상태로 유지됩니다. 항상 켜기 - Android Keystore는 암호를 안전하게 저장하는 데 사용됩니다 - 알림 서비스가 작동할 수 있습니다. - 앱을 다시 시작하거나 암호를 변경한 후 Android Keystore를 사용하여 암호를 안전하게 저장합니다. - 알림을 받을 수 있습니다. + Android 암호 저장소는 암호를 안전하게 저장하는 데 사용됩니다 - 알림 서비스가 작동할 수 있습니다. + 앱을 다시 시작하거나 암호를 변경한 후 Android 암호 저장소를 사용하여 암호를 안전하게 저장합니다. - 알림을 받을 수 있습니다. 앱이 실행 중일 때만 알림을 받을 수 있으며, 백그라운드 서비스는 시작되지 않습니다. 앱 데이터 백업 앱 아이콘 각각의 채팅 프로필에 사용될 겁니다.]]> - 별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 각각의 대화 상대 및 그룹 구성원에게 사용될 겁니다. -\n참고: 연결이 많은 경우 배터리 및 트래픽 소비가 높을 수 있고 일부 연결이 실패할 수 있습니다. + 각각의 대화 상대 및 그룹 멤버에게 사용될 겁니다. \n참고: 연결이 많은 경우 배터리 및 트래픽 소비가 높을 수 있고 일부 연결이 실패할 수 있습니다.]]> 이미지 수신 요청됨 음성 및 영상 통화 - 음성 통화 (종단간 암호화 아님) + 음성 통화 (종단 간 암호화 아님) 인증을 사용할 수 없음 배터리 최적화가 활성화되어, 백그라운드 서비스 및 새 메시지에 대한 주기적 요청이 꺼집니다. 설정을 통해 다시 활성화할 수 있습니다. 배터리에 가장 좋음. 앱이 실행 중일 때만 알림을 받게 됩니다 (백그라운드에서 실행되지 않음).]]> 설정을 통해 비활성화할 수 있습니다. – 앱이 실행되는 동안 알림이 표시됩니다.]]> 당신과 대화 상대 모두 사라지는 메시지를 보낼 수 있습니다. - 데이터베이스 암호를 저장하고 있는 Keystore에 접근할 수 없습니다. - 배터리 더욱 사용! 백그라운드 서비스가 항상 실행됩니다. - 메시지를 수신되는 즉시 알림이 표시됩니다.]]> + 데이터베이스 암호를 저장하고 있는 암호 저장소에 접근할 수 없습니다. + 배터리를 더욱 사용함! 앱이 항상 백그라운드에서 실행됩니다. - 수신되는 즉시 알림이 표시됩니다.]]> 통화 종료됨 %1$s 전화 중… 전화 연결 중 @@ -180,7 +178,6 @@ 채팅 데이터베이스 대화 상대를 초대할 수 없습니다! 변경 - 채팅 기록 보관함 역할 변경 채팅 데이터베이스가 삭제됨 채팅이 멈춤 @@ -194,11 +191,10 @@ 연결 중 (도입) 연결 오류 연결 %1$d - 링크를 통해 연결하시겠습니까\? - 대화 상대와 모든 메시지가 삭제됩니다. - 삭제 후 되돌릴 수 없습니다! - 대화 상대와 종단간 암호화됨 + 주소를 통해 연결하시겠습니까? + 대화 상대와 모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 대화 상대와 종단 간 암호화됨 대화 상대와 아직 연결되지 않았습니다! - %1$s에 생성 완료 비밀 그룹 생성 익명 수락 1개월 @@ -220,7 +216,7 @@ 삭제 대기 중인 연결을 삭제할까요\? 인증 지우기 - 데이터베이스 비밀구절(passphrase) & 내보내기 + 데이터베이스 암호 & 내보내기 서버 삭제 주소 삭제 주소를 삭제할까요\? @@ -228,13 +224,11 @@ 탈중앙화 개발자 도구 기기 - 데이터베이스 비밀구절(passphrase) + 데이터베이스 암호 모든 채팅 프로필 파일 삭제 데이터베이스 에러 - 데이터베이스 비밀구절(passphrase)이 Keystore에 저장된 것과 일치하지 않습니다. - 채팅을 열려면 데이터베이스 비밀구절(passphrase)이 필요합니다. - 보관된 채팅 삭제 - 보관된 채팅을 삭제할까요\? + 데이터베이스 암호가 암호 저장소에 저장된 것과 일치하지 않습니다. + 채팅을 열려면 데이터베이스 암호가 필요합니다. %d 개의 대화 상대가 선택되었습니다. 데이터베이스 ID 다음 채팅 프로필 삭제 @@ -242,12 +236,12 @@ 다음 기간 이후 자동 삭제 위, 다음 : 데이터베이스 삭제 - 데이터베이스는 임의의 비밀구절(passphrase)로 암호화되었습니다. 내보내기 기능 사용 전 비밀구절을 변경해 주세요. - 파일과 미디어를 삭제할까요\? - 현재 비밀구절(passphrase)… + 데이터베이스는 임의의 암호로 암호화되었습니다. 내보내기 기능 사용 전 암호를 변경해 주세요. + 파일 및 미디어를 삭제하겠습니까? + 현재 암호… 데이터베이스 암호화 완료! - 데이터베이스 암호화 비밀구절(passphrase)이 업데이트됩니다. - 데이터베이스는 임의의 비밀구절(passphrase)로 암호화되었고, 원하시면 변경할 수 있습니다. + 데이터베이스 암호화 암호가 업데이트됩니다. + 데이터베이스는 임의의 암호로 암호화되며 변경할 수 있습니다. 데이터베이스는 암호화될 것입니다. 메시지 삭제 다음 기간 이후 자동 삭제 @@ -260,14 +254,14 @@ %d일 그룹 삭제 주소 변경됨 - 데이터베이스 암호화 비밀구절(passphrase)이 업데이트되며 Keystore에 보관됩니다. - 데이터베이스는 암호화되고, 비밀구절(passphrase)은 Keystore에 보관됩니다. + 데이터베이스 암호화 암호가 업데이트되며 암호 저장소에 보관됩니다. + 데이터베이스는 암호화되고, 암호는 암호 저장소에 보관됩니다. 채팅 프로필을 삭제할까요\? 모든 파일 삭제 채팅 프로필을 삭제할까요\? 모두에게서 삭제 그룹을 삭제할까요\? - 표시 이름이 중복되어요! + 표시 이름이 중복됩니다! 연결 끊기 기기 인증이 비활성화되어 SimpleX 잠금 기능이 작동하지 않아요. SimpleX 잠금 비활성화 @@ -276,27 +270,27 @@ 새로운 채팅 시작 표시 이름 표시 이름에는 공백문자가 쓰일 수 없어요. - 표시 이름 - 종단간 암호화된 음성 전화 - 종단간 암호화된 영상 전화 + 이름을 입력: + 종단 간 암호화된 음성 전화 + 종단 간 암호화된 영상 통화 비활성화 - 종단간 암호화 + 종단 간 암호화 중복된 메시지 1일로 설정 사라지는 메시지 %d 개의 파일 총 크기 %s 다이렉트 이 채팅에서는 사라지는 메시지를 사용할 수 없습니다. - 이 그룹에서는 사라지는 메시지를 사용할 수 없습니다. + 사라지는 메시지를 사용할 수 없습니다. %d분 %d 개월 %d 분 %d 개월 %d 주 다운그레이드하고 채팅 열기 - 1:1 메시지 + 다이렉트 메시지 사라지는 메시지 - 이 그룹에서는 멤버들의 1:1 채팅이 금지되어 있어요. + 이 그룹에서는 멤버들의 다이렉트 메시지가 금지되어 있어요. %d초 %d 초 %d시 @@ -307,7 +301,7 @@ 기기 인증을 하고 있지 않아요. 기기 인증을 켜면 설정에서 SimpleX 잠금 기능을 사용할 수 있어요. %d 시간 %d 시간 - 앱/데이터베이스의 다른 마이그레이션: %s / %s + 앱/데이터베이스의 다른 이전: %s / %s 다른 이름, 아바타 그리고 전송 격리. 다시 보지 않기 이 대화 상대로부터의 메시지를 수신할 서버와 연결되었어요. @@ -336,7 +330,7 @@ 환영 메시지 그룹 프로필 수정 그룹 나가기 - 1:1 채팅 시작하기 + 다이렉트 메시지 보내기 서버 인다이렉트 (%1$s) 허용함 @@ -357,12 +351,7 @@ 설정에서 잠금 화면에서 바로 전화를 받을 수 있도록 설정할 수 있어요. 연결을 완료하려면 대화 상대가 온라인 상태여야 해요. \n연결 요청을 취소하고 대화 상대를 삭제할 수 있어요 (그리고 새 링크로 재시도). - 다음과 같은 경우에 발생할 수 있어요. -\n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우 -\n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우 -\n3. 침해된 연결의 경우 -\n서버 업데이트를 받으려면 설정에서 개발자에게 연락해 주세요. -\n저희 개발팀은 메시지 손실을 방지하기 위해 중복된 서버를 추가할 예정이에요. + 다음과 같은 경우에 발생할 수 있습니다. \n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우 \n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우 \n3. 침해된 연결의 경우 SimpleX 잠금 켜짐 응답됨… 확인 받음… @@ -375,12 +364,12 @@ 프로필 생성 오류! 그룹 링크로 익명 채팅 그룹 링크로 채팅 - 일회용 링크로 채팅 + 일회용 링크를 통해 일회용 익명 링크를 공유했어요. 일회용 링크를 공유했어요. 상대의 연락처 링크로 익명 연결 상대의 연락처 링크로 연결 - 일회용 연락처로 익명 연결 + 일회용 링크로 익명 연결 SMP 서버 주소가 올바른 형식이고 줄로 구분되어 있고 중복이 없는지 확인해 주세요. SMP 서버 저장 오류 네트워크 설정 업데이트 오류 @@ -403,13 +392,13 @@ 테스트가 %s단계에서 실패했어요. 서버는 대기열을 생성하고 비밀번호를 확인하려면 인증이 필요해요. 알림을 받으려면 데이터베이스 암호를 입력해 주세요. - 비밀번호가 필요해요. - 프로필 삭제 오류 + 암호가 필요해요. + 사용자 프로필 삭제 오류 사용자 개인정보 업데이트 오류 데이터베이스가 올바르게 작동하지 안하요. 자세히 알아보려면 탭하세요. 수정하기 - 메시지가 삭제돼요. 삭제 후 복구할 수 없어요! - 메시지가 삭제 표시될 거예요. 대화 상대는 여전히 삭제된 내용을 볼 수 있어요. + 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 메시지가 삭제 표시됩니다. 수신자는 여전히 삭제된 내용을 볼 수 있습니다. WebRTC ICE 서버 주소가 올바른 형식이고 줄로 구분되고 중복이 없는지 확인해 주세요. ICE 서버(한 줄에 하나씩) ICE 서버 저장 오류 @@ -425,39 +414,39 @@ 데이터베이스 내보내기 자동 삭제되는 메시지를 사용할까요\? 설정 변경 오류 - 이 작업은 되돌릴 수 없어요. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제돼요. 이는 몇 분 걸릴 수 있어요. + 이 결정은 되돌릴 수 없습니다. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제됩니다. 이는 몇 분 걸릴 수 있습니다. 오류: %s - 올바른 비밀번호를 입력해 주세요. - 데이터베이스 비밀번호 변경이 완료되지 않았어요. + 올바른 암호를 입력해 주세요. + 데이터베이스 암호 변경이 완료되지 않았어요. 데이터베이스 오류 복구 그룹 링크 생성 오류 그룹 링크 업데이트 오류 역할 변경 오류 멤버 삭제 오류 데이터베이스 다운그레이드 - 마이그레이션: %s - 모든 멤버에게서 그룹이 삭제돼요. 삭제 후 복구할 수 없어요! - 나에게서만 그룹이 삭제되요. 삭제 후 복구할 수 없어요! + 이전: %s + 모든 멤버에게서 그룹이 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 나에게서만 그룹이 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 파일을 찾을 수 없음 사용자 비밀번호 저장 오류 채팅 정지하기 오류 - 채팅 데이터베이스 내보내기 오류 - 채팅 데이터베이스가 암호화되지 않았어요. 비밀번호를 설정하여 보호해 주세요. - 비밀번호를 입력해 주세요… + 채팅 데이터베이스를 내보내는 동안 오류 + 채팅 데이터베이스가 암호화되지 않았어요. 암호를 설정하여 보호해 주세요. + 암호를 입력해 주세요… 검색에 비밀번호 입력 이미지 수정하기 - 이 작업은 실행 취소될 수 없어요. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실돼요. + 이 결정은 되돌릴 수 없습니다. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실됩니다. 채팅 데이터베이스 가져오기 오류 데이터베이스를 암호화할까요\? 데이터베이스 ID 및 전송 격리 옵션. 채팅 시작하기 오류 데이터베이스 암호화 오류 - 올바른 현재 비밀번호를 입력해 주세요. + 올바른 현재 암호를 입력해 주세요. 채팅 프로필 삭제 프로필 삭제 경고: 일부 데이터가 손실될 수 있어요! 데이터베이스 업그레이드 - 이 작업은 실행 취소될 수 없어요. 수신 및 전송된 모든 파일과 미디어가 삭제돼요. 저해상도 사진만 삭제되지 않아요. + 이 결정은 되돌릴 수 없습니다. 수신 및 전송된 모든 파일과 미디어가 삭제됩니다. 저해상도 사진은 삭제되지 않습니다. 채팅 데이터베이스 삭제 오류 그룹 링크 삭제 오류 파일 저장 오류 @@ -488,14 +477,14 @@ 그룹으로 초대 %1$s 그룹 링크 환영 메시지 - 보여지는 그룹 이름 + 그룹 이름 입력: 그룹 이름 : - 그룹은 완전히 탈중앙화되어 있으며 구성원만 그룹을 볼 수 있어요. - 프로필이 그룹 구성원에게 전송될 거예요. + 완전히 탈중앙화됨 – 멤버만 볼 수 있습니다. + 프로필이 그룹 멤버에게 전송될 거예요. 그룹 프로필은 서버가 아닌 멤버들의 기기에 저장되어요. 그룹 설정 - 그룹 구성원은 사라지는 메시지를 보낼 수 있습니다. - 그룹 멤버들끼리 1:1 채팅을 할 수 있어요. + 그룹 멤버는 사라지는 메시지를 보낼 수 있습니다. + 그룹 멤버들끼리 다이렉트 메시지를 보낼 수 있어요. 멤버 초대하기 비활성 그룹 관찰자 @@ -513,7 +502,7 @@ 마크다운 사용법 SimpleX 작동 방식 그룹 초대가 만료되었어요. - 그룹 멤버는 보낸 메시지를 영구 삭제할 수 있어요. + 그룹 멤버는 보낸 메시지를 영구 삭제할 수 있습니다. (24 시간) 그룹 멤버는 음성 메시지를 보낼 수 있어요. 숨긴 프로필 비밀번호 작동 방식 @@ -545,7 +534,7 @@ 이미지 수가 너무 많아요! 거절해도 상대에게 알림이 전송되지 않아요. 영상 통화에서 QR 코드를 보여주거나 링크를 공유해 주세요.]]> - 영상 전화 + 영상 통화 영상 끄기 스피커 켜기 영상 켜기 @@ -556,7 +545,7 @@ 대화 상대가 업로드를 완료하면 이미지가 수신될 거예요. 프로필 이미지 하나의 프로필로 여러 사람과 연락할 필요 없이 무수히 많은 익명 프로필로 연락할 수 있어요. - 스팸 및 남용에 면역 + 스팸 방지 무시하기 SimpleX Chat 초대 링크를 받으면 브라우저에서 참여할 수 있어요 : 링크 미리보기 이미지 @@ -586,16 +575,16 @@ 그룹 호환되지 않는 데이터베이스 버전 그룹에 참여 중 - 익명 모드는 기본 프로필 이름과 사진과 같은 개인 정보를 보호해줘요. 새 대화 상대마다 새로운 랜덤 프로필이 만들어져요. + 익명 모드는 대화 상대마다 새로운 무작위 프로필을 사용하여 개인 정보를 보호합니다. %s 은(는) 인증되었어요. 기울게 익명 프로필 사용 중 초대받은 그룹에 참여하면, 그 그룹에서도 동일한 익명 프로필이 사용되어요. - 내 랜덤 프로필 + 내 무작위 프로필 음성 전화 옴 %s은(는) 인증되지 않았어요. 터미널용 SimpleX Chat를 설치하세요 - 영상 전화 옴 - 잘못된 마이그레이션 확인 + 영상 통화 옴 + 잘못된 이전 확인 익명 모드로 참여 잘못된 QR 코드 잘못된 보안 코드! @@ -617,7 +606,7 @@ 연락처 이름 및 메시지 숨기기 켜기 대화 상대가 나와의 연결을 삭제했을 가능성이 커요. - 메시지 전달 오류 + 메시지 전송 오류 조정 모든 멤버에게서 메시지가 삭제될 거예요. 이 메시지는 모든 멤버에게 조정됨으로 표시될 거예요. @@ -634,12 +623,11 @@ 메시지 이 설정은 현재 내 프로필의 메시지에 적용되어요. 멤버 - 역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요. - 되돌리기 + 역할이 %s(으)로 변경되고, 멤버는 새로운 초대를 받게 될 거예요. 이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요. 나가기 큰 파일! - 네트워크 설정 + 고급 설정 연결하려면 Onion 호스트가 필요해요. 핑 횟수 핑 간격 @@ -667,23 +655,19 @@ 사용 가능한 경우 Onion 호스트가 사용될 거예요. Onion 호스트가 사용되지 않을 거예요. 전송 격리 - Onion 호스트가 사용되지 않을 거예요. - 사용 가능한 경우 Onion 호스트가 사용될 거예요. - 연결하려면 Onion 호스트가 필요해요. 차세대 사생활 보호 메시징 - 새 비밀번호… + 새 암호… TCP 연결 유지 활성화 %s의 새로운 기능 마크다운 도움말 - SimpleX에는 사용자 식별자가 없는데도 어떻게 메시지를 전달할 수 있어요\?]]> 그룹에서 나갈까요\? - 데이터베이스 버전이 앱보다 최신이지만, 다음에 대한 다운 마이그레이션 없음: %s - 멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요! - 역할이 \"%s\"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다. + 앱 버전보다 최신 버전의 데이터베이스를 사용하고 있지만 데이터베이스를 다운그레이드할 수 없습니다: %s + 멤버가 그룹에서 제거됩니다. 이 결정은 되돌릴 수 없습니다! + 역할이 "%s"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다. 기본값으로 재설정 메시지 내용 대화 상대 이름 및 메시지 표시 - 나만 메시지를 영구 삭제할 수 있어요(대화 상대는 \"삭제됨\" 표시만 할 수 있음). + 나만 메시지를 영구 삭제할 수 있어요(대화 상대는 "삭제됨" 표시만 할 수 있음). 이 링크를 보낸 상대에게 프로필이 전송될 거예요. 파일 수신은 아직 지원되지 않아요. 올바른 링크를 사용했는지 확인하거나 상대에게 다른 링크를 보내달라고 말해 주세요 @@ -694,18 +678,17 @@ SimpleX Chat 메시지 그룹 관리자에게 문의해 주세요. 메시지를 보낼 수 없습니다! - + OK 거절 비밀번호 표시 2계층 종단 간 암호화 로 전송된 사용자 프로필, 연락처, 그룹 및 메시지를 저장되어요.]]> - 자세한 내용은 GitHub에서 확인해 주세요. - 개인 정보 및 보안 + 개인 정보 보호 및 보안 알림은 앱이 중지되기 전까지만 전달될 거예요! 당신만 사라지는 메시지를 보낼 수 있습니다. 사라지는 메시지 전송은 허용되지 않습니다. 음성 메시지 허용되지 않음. 사라지는 메시지 전송은 허용되지 않습니다. - 이전 데이터베이스 기록 + 이전 데이터베이스 보관함 %1$s 초대됨 나감 강퇴됨 @@ -713,20 +696,20 @@ 그룹 소유자만 그룹 설정을 변경할 수 있어요. 다음을 통해 수신 대화 상대만 사라지는 메시지를 보낼 수 있습니다. - 멤버들 간의 1:1 채팅이 허용되지 않음. + 멤버들 간의 다이렉트 메시지가 허용되지 않음. 나만 음성 메시지를 보낼 수 있어요. 대화 상대만 음성 메시지를 보낼 수 있어요. 메시지 영구 삭제 허용되지 않음. 음성 메시지 허용되지 않음. - 상대만 메시지를 영구 삭제할 수 있어요(나는 \"삭제됨\"으로 표시만 할 수 있음). + 상대만 메시지를 영구 삭제할 수 있어요(나는 "삭제됨"으로 표시만 할 수 있음). 그룹 소유자만 음성 메시지를 사용 가능하도록 설정할 수 있어요. 일회성 초대 링크 붙여넣기 프로필은 대화 상대들하고만 공유됩니다. 프라이버시의 재정의 - 오픈 소스 프로토콜과 코드 - 누구나 자신만의 서버를 구축할 수 있어요. + 누구나 서버를 호스팅할 수 있습니다. 앱이 실행 중일 때 - GitHub 에서 확인해 주세요.]]> + GitHub 에서 확인해 주세요.]]> 릴레이 서버는 IP 주소를 숨겨주지만, 통화 시간을 관찰 할 수 있어요. 그룹 링크로 초대 설정을 통해 나중에 변경할 수 있어요. @@ -736,7 +719,7 @@ 열기 앱 잠금 %1$s님, 환영합니다! - (그룹 구성원에게만 저장됨) + (그룹 멤버에게만 저장됨) 앱 평가하기 주기적 즉시 @@ -756,13 +739,11 @@ 저장하고 그룹 멤버들에게 알리기 저장하고 대화 상대에게 알리기 지우기 - 아카이브 저장하기 - 색상 저장하기 - 암호 저장소에 비밀번호 저장하기 + 암호 저장소에 암호 저장하기 데이터베이스 백업 복원하기 - 데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요. + 데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주십시오. 이 결정은 되돌릴 수 없습니다. 데이터베이스 백업을 복원할까요\? - 키스토어에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요. + 암호 저장소에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요. 저장하고 그룹 프로필 업데이트하기 환영 메시지를 저장할까요\? 그룹 프로필 저장하기 @@ -771,13 +752,13 @@ 코드 스캔하기 대화 상대의 앱에서 보안 코드를 스캔해 주세요. 저장된 WebRTC ICE 서버가 제거될 거예요. - 비밀번호 저장하고 채팅 열기 + 암호를 저장하고 채팅 열기 역할 설정을 저장할까요\? 프로필 비밀번호 저장하기 가져온 채팅 데이터베이스를 사용하려면 앱을 다시 실행해 주세요. 새 프로필을 만드려면 앱을 다시 실행해 주세요. - 암호 저장소에서 비밀번호를 삭제할까요\? + 암호 저장소에서 암호를 삭제할까요? 채팅 기능 실행하기 복원하기 대화 상대가 파일 전송을 취소했어요. @@ -810,7 +791,7 @@ 파일 공유… 이미지 공유… 메시지 공유… - 라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줘요. + 라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줍니다. 보내기 초대 링크 공유 보안 코드 @@ -859,18 +840,17 @@ 메시지 및 파일 익명 모드 실험적 - 내보낼 비밀번호 설정 + 내보낼 암호 설정 SMP 서버 미리 설정된 서버 주소 내 서버 내 서버 주소 %1$s을(를) 강퇴했어요. 채팅 데이터베이스를 내보내기, 가져오기 또는 삭제 하려면 채팅 기능을 중지해 주세요. 채팅 기능이 중지된 동안에는 메시지를 주고받을 수 없어요. - 비밀번호를 모르면 변경하거나 찾을 수 없으므로 비밀번호를 안전하게 보관해 주세요. + 암호를 모르면 변경하거나 찾을 수 없으므로 암호를 안전하게 보관해 주세요. 제출하기 - 비밀번호를 모르면 채팅에 액세스할 수 없으니 비밀번호를 안전하게 보관해 주세요. + 암호를 모르면 채팅에 액세스할 수 없으니 암호를 안전하게 보관해 주세요. 채팅 기능을 중지할까요\? - 데이터베이스 작업을 할 수 있도록 채팅 기능을 중지하기 수신 주소 바꾸기 복호화 오류 패스코드 확인 @@ -889,7 +869,7 @@ 사용자화 전송 - 자폭 패스코드 변경 + 자체 소멸 패스코드 변경 모든 앱 데이터가 삭제되었습니다. 인증 패스코드 변경 @@ -902,12 +882,12 @@ 다른 사용자와 연결할 수 있도록 주소를 만듭니다. 커스텀 테마 자동 수락 - 자폭 모드 변경 + 자체 소멸 모드 변경 (현재) 다크 테마 메시지 반응을 허용합니다. 당신과 대화 상대 모두 메시지 반응을 추가할 수 있습니다. - 대화 상대가 당신에게 전화할 수 있도록 허용합니다. + 대화 상대가 당신에게 통화할 수 있도록 허용합니다. 현재 패스코드 SimpleX 주소에 대하여 인증 취소됨 @@ -922,12 +902,12 @@ 대화 상대가 허용하는 경우에만 메시지 반응을 허용합니다. 모든 대화가 연결된 상태로 유지됩니다. 프로필 업데이트가 대화 상대에게 전송됩니다. 전송된 메시지는 설정된 시간이 지나면 삭제됩니다. - 앱 패스코드가 자체소멸 패스코드로 대체되었습니다. + 앱 패스코드가 자체 소멸 패스코드로 대체되었습니다. 음성/영상 통화가 허가되지 않았습니다. 잘못된 메시지 해쉬 인증 실패 카메라 - 대화 상대가 메시지 응답을 추가할 수 있도록 허용합니다. + 대화 상대가 메시지 반응을 추가할 수 있도록 허용합니다. 잘못된 메시지 아이디 당신과 대화 상대 모두 전화를 걸 수 있습니다. 앱 패스코드 @@ -939,4 +919,607 @@ 이미지 기다리는 중 음성 메시지 %1$d개의 메시지의 해독에 실패했습니다. - \ No newline at end of file + 추가 강조 색상 2 + 백업 복원 후 암호화 수정. + 채팅 더 빠르게 찾기 + 그룹 링크 + 읽지 않은 채팅과 즐겨찾기 채팅 필터링. + 고급 설정 + 연결 유지하기 + 소유자 + %1$d개의 메시지가 %2$s에 의해 삭제되었습니다 + 6개의 새로운 인터페이스 언어 + 연락처 추가 + 관리자 + 관리자는 모든 멤버를 위해 특정 멤버를 차단할 수 있습니다. + 메시지 전송 확인서! + 우리가 놓친 두 번째 체크! ✅ + 주소 변경 중지 + - 최대 5분의 음성 메시지. +\n- 사용자 정의 소멸 시간. +\n- 편집 기록. + 사용자 여러분께 감사드립니다 – Weblate를 통해 기여하세요! + 주소 변경이 중지됩니다. 이전 수신 주소가 사용됩니다. + 일본어 및 포르투갈어 UI + 주소 변경을 중지하시겠습니까? + 모든 멤버 + 위해 활성화됨 + 새로운 기능 + 더 보기 + 보안 평가 + SimpleX Chat 보안은 Trail of Bits에 의해 감사되었습니다. + 이미 연결 중입니다! + %1$d개 기타 파일 오류. + %1$d개 파일이 다운로드되지 않았습니다. + %1$d개의 파일을 다운로드하지 못했습니다. + 암호화 동의 중… + 몇 가지 더 + 허용 + %1$s개의 메시지가 전송되지 않았습니다. + %1$d개의 파일이 아직 다운로드 중입니다. + %1$d개 파일이 삭제되었습니다. + 확인됨 + 승인 오류 + SimpleX 링크 전송을 허용합니다. + 앱 데이터 이전 + 모든 연락처, 대화 및 파일은 안전하게 암호화되어 구성된 XFTP 릴레이에 일괄 업로드됩니다. + 활성 연결 + 모든 프로필 + 적용 + 모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 파일 및 미디어 전송을 허용합니다. + 새로운 무작위 프로필이 공유됩니다. + 모든 색상 모드 + 항상 + 외 %d개 이벤트 + 앱이 새 로컬 파일 (비디오 제외)을 암호화합니다. + 이미 그룹에 있습니다! + 항상 프라이빗 라우팅 사용. + %s에 대한 암호화에 동의 중… + %s의 새 메시지는 모두 숨겨집니다! + 통화를 허용할까요? + 다운그레이드 허용 + 채팅 + 데스크톱과의 연결이 잘못된 상태입니다. + 보관 및 업로드 + 연결 중 + %1$d항목의 파일 오류:\n%2$s + 생성 + 올바른 이름을 %s 로 지정하시겠습니까? + 카메라 + 카메라와 마이크 + 연락처 + 연락처 %1$s가 %2$s (으)로 변경됨 + 직접 연결하시겠습니까? + 적용 대상 + 무작위 프로필을 사용하여 그룹을 만듭니다. + 잘못된 데스크톱 주소 + 연결이 종료됨 + 연결됨 + 연결 + 음성 통화 + 곧 출시 예정입니다! + 채팅 데이터베이스 + 채팅 테마 + 친구들과 더 빠르게 연결하세요. + 연결이 중지됨 + 데스크톱에 연결됨 + 데스크톱에 연결 중 + 자신과 연결하겠습니까? + 채팅이 이전되었습니다! + - 디렉터리 서비스(베타)에 연결하세요!\n- 전송 알림(최대 20명).\n- 더 빠르고 안정적입니다. + 이전 취소 + 이전하려는 데이터베이스의 암호를 기억하고 있는지 확인합니다. + 익명 모드로 연결 + 베타 + 앱 패스코드 + 계속 + 채팅이 중지되었습니다. 다른 기기에서 이 데이터베이스를 이미 사용하고 있다면, 채팅을 시작하기 전에 다시 전송해야 합니다. + 직접 연결됨 + 일괄 다운로드됨 + %1$s과(와) 연결하시겠습니까? + 앱 테마 + 앱 업데이트가 다운로드되었음 + 아랍어, 불가리아어, 핀란드어, 히브리어, 태국어 및 우크라이나어 - 사용자와 Weblate 덕분입니다. + 향상된 그룹 기능 + 데이터베이스를 저장 중 + 저자 + 용량 초과 - 수신자가 이전에 보낸 메시지를 받지 못했습니다. + 셀룰러 + 채팅 색상 + 업데이트 확인 + 인터넷 연결을 확인하고 다시 시도하십시오 + 다른 기기에서 이전을 선택하고 QR 코드를 스캔합니다.]]> + 완료 + 알 수 없는 서버의 파일을 확인합니다. + 연결된 서버 + 연결 및 서버 상태. + %s 과의 연결이 잘못된 상태입니다.]]> + 네트워크 관리 + 수신 주소를 변경하겠습니까? + 데스크톱에 연결 + 메시지를 보낼 수 없음 + 연락처 삭제를 확인하시겠습니까? + 연락처가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 대화가 삭제되었습니다! + 연락처가 삭제되었어요! + 파일 선택 + 카메라를 사용할 수 없음 + 그룹 생성: 새로운 그룹을 생성합니다.]]> + 연락처 추가 : 새 초대 링크를 만들거나 받은 링크를 통해 연결합니다.]]> + 개인 메모를 지우시겠습니까? + XFTP 서버 구성 + 나중에 채팅할 수 있도록 연락처를 보관합니다. + 연결된 데스크톱 + 일괄 업로드됨 + 보관된 연락처 + SMP 서버 구성 + 채팅 프로필 생성 + 그룹 멤버에 전화를 걸 수 없음 + 자동 연결 + 연결이 중지됨 + 시도 + 앱 세션 + 업데이트 확인 + 주소 필드 근처에 있는 정보 버튼을 클릭하여 마이크를 사용할 수 있습니다. + 모서리 + 채팅 데이터베이스를 내보냈습니다 + 연락처에 전화를 걸 수 없음 + 연락처에 연결하니, 잠시 기다리거나 나중에 확인하십시오! + 연락처가 삭제됩니다. + 색상 모드 + 복사 오류 + 향상된 통화 기능 + 향상된 메시지 날짜 기능 + 향상된 보안 ✅ + 링크로 연결하시겠습니까? + 네트워크 설정 확인 + 일괄 삭제됨 + 업로드 확인 + 모바일에 연결 + 연결된 모바일 + 연결 + 그룹 멤버에게 메시지를 보낼 수 없음 + 전송 알림을 활성화하시겠습니까? + 암호화 OK + 데스크톱 + 기능 처리 시간이: %1$d 초 이상: %2$s + 주소를 만들지 않음 + 기기 + 파일 및 미디어 금지됨! + 링크 생성 중… + 전달 서버 %1$s에 연결하는 동안 오류가 발생했습니다. 나중에 다시 시도하십시오. + %d 초 + 모든 그룹에 사용 안 함 + 사용 안 함 (그룹 변경 사항 유지) + 데스크톱 앱에서 새 프로필 생성합니다. 💻 + 오류 + 주소 변경을 중단하는 중 오류 + 메시지를 전송하는 동안 오류 + 멤버의 %d 메시지를 삭제하시겠습니까? + 파일을 찾을 수 없음 - 파일이 삭제되었거나 취소되었을 가능성이 큽니다. + 통화 + 다운로드 + 프록시에서 자격 증명을 사용하지 마십시오. + 프록시를 저장하는 중 오류 + 이어폰 + 블루투스 + 암호화 재협상 실패 + 자체 소멸 활성화 + 활성화 (변경 사항 유지) + 전송 알림을 비활성화하시겠습니까? + 그룹 전송 알림을 비활성화하시겠습니까? + 모두 사용 안 함 + 모두 활성화 + 데이터베이스가 암호화되고 암호가 설정에 저장됩니다. + %s 차단됨 + %s 의 암호화 재협상 필요 + %s 의 암호화 재협상 허용 + 멤버 블록 오류 + 그룹 검색 및 참여하기 + 더 빠른 참여와 더 안정적인 메시지. + 전송 알림! + 활성화 + 데스크톱 연결을 끊으시겠습니까? + 데스크톱에 잘못된 초대 코드가 잘못되었습니다 + 데스크톱 연결이 끊어졌습니다 + 다운로드 실패 + 채팅 데이터베이스를 내보내는 동안 오류 + 파일 + 오류 + 다운로드됨 + 만료 + 삭제됨 + 삭제 오류 + 새 멤버에게 내역을 보내지 마십시오. + 삭제 및 연락처에 알림 + 차단됨 + 관리자에 의해 차단됨 + %d 개의 메시지가 차단됨 + %d 개의 메시지가 관리자에 의해 차단됨 + %1$s의 대상 서버 주소가 전달 서버 %2$s의 설정과 호환되지 않습니다. + 대상 서버 버전 %1$s이 전달 서버 %2$s와 호환되지 않습니다. + 확장 + 카메라 권한 활성화 + 잠금 활성화 + 로컬 파일 암호화 + 암호화 재협상 필요 + 멤버 연락처를 만드는 동안 오류 + 초대장을 보내는 중 오류 + 비활성화됨 + 모두 차단 + 이 멤버를 차단하시겠습니까? + 저장된 파일 & 미디어 암호화 + 한 번에 최대 20개의 메시지를 삭제할 수 있습니다. + 더 나은 개인정보 보호를 위한 흐리기 + 오류 + %s 에서 연결이 끊어졌습니다]]> + 데스크톱 기기 + 데스크톱 버전이 지원되지 않습니다. 두 기기가 동일한 버전에 있는지 확인하십시오. + 암호 입력 + 보관함을 다운로드하는 동안 오류 + 보관함을 업로드하는 중 오류 + 설정을 저장하는 중 오류 + 내보낸 파일이 없음 + 이 기기에서 데이터베이스 삭제 + 경고: 보관된 데이터가 삭제됩니다.]]> + 상세 + 서버를 다시 연결하는 중 오류 + 다운로드 오류 + 사용 안 함 + 전송 알림을 활성화하는 동안 오류! + 데스크톱이 비활성 상태입니다 + 보관함 다운로드 중 + 보관 링크를 생성 중 + 파일 오류 + 프로필 생성 + 파일 및 미디어 + 로컬 네트워크를 통해 탐색 가능 + 암호화 재협상 오류 + %d 메시지를 삭제하시겠습니까? + 다운로드 + GitHub에서 새 버전을 다운로드합니다. + 데스크톱 주소 + 패스코드 + 모든 그룹에 활성화 + 활성화 (그룹 변경 사항 유지) + 파일 + 삭제된 연락처 + %s 의암호화에 동의함 + %s 의 암호화 OK + 상대가 온라인 상태가 될 때까지 기다릴 필요가 없습니다! + 그룹 멤버 차단 + 현재 프로필 + SMP 서버를 로드하는 중 오류 + 개인 메모를 삭제하는 동안 오류 + 데이터베이스를 삭제하는 동안 오류 + 다음과 같은 이유로 끊어졌습니다: %s + 통화 종료 + 알림을 표시하는 동안 오류가 발생하였으니, 개발자에게 문의하십시오. + %d 분 + %d 개의 메시지가 삭제됨 + XFTP 서버를 저장하는 중 오류 + 알림 비활성화 + 패스코드 입력 + 주소를 설정하는 중 오류 + 좋아함 + 연결을 동기화하는 중 오류 + 콘텐츠를 표시하는 중 오류 + 메시지를 표시하는 중 오류 + 미디어 흐리기 + 참고: 두 기기에서 동일한 데이터베이스를 사용하면 연결된 사람들의 메시지 복호화가 깨질 수 있으며, 이는 보안 보호 조치입니다.]]> + 다크 + 데이터베이스 이전이 진행 중입니다.\n이 작업은 몇 분 정도 걸릴 수 있습니다. + 대상 서버 오류: %1$s + 전송 + 상세 통계 + 개발자 옵션 + 사용자 또는 대상의 서버가 프라이빗 라우팅을 지원하지 않는 경우에도 메시지를 직접 보내지 마십시오. + 알림 없이 삭제 + 프로필을 전환하는 중 오류 + 삭제 완료 + 에 사라짐 + 다크 모드 + 로컬 네트워크를 통해 탐색 + 차단됨 + 다이렉트 채팅에서 활성화함 (베타)! + 자체 소멸 + 환영 메시지 입력…(선택사항) + WebView를 초기화하는 중 오류가 발생했습니다. WebView가 설치되어 있고 지원되는 아키텍처가 arm64인지 확인합니다.\n오류: %s + XFTP 서버를 로드하는 중 오류가 발생했습니다. + 세부 정보를 로드하는 중 오류 + 통계를 재설정하는 중 오류 + 대화에서 비활성화 된 경우에도 마찬가지입니다. + 테마 내보내기 + 삭제 완료: %s + 보낸 날짜: %s + 관리자에 의해 차단됨 + 비활성화됨 + 치명적 오류 + 오류: %1$s + WebView를 초기화하는 중 오류가 발생했습니다. 시스템을 새 버전으로 업데이트하십시오. 개발자에게 문의하세요.\n오류: %s + 오류 + 생성 완료 + 암호화에 동의함 + 암호화 재협상 허용 + %s :에 사라짐 + 멤버 차단 + 멤버를 차단하시겠습니까? + 통화 금지! + 전송 알림이 더 이상 유효하지 않습니다! + 연결 끊기 + 데스크톱 앱 버전 %s은(는) 이 앱과 호환되지 않습니다. + 모바일 연결 끊기 + 데스크톱이 사용 중입니다 + 서버를 다시 연결하는 중 오류 + 메시지를 만드는 동안 오류 + 참고: 메시지 및 파일 릴레이는 SOCKS 프록시를 통해 연결됩니다. 통화 및 전송 링크 미리 보기는 직접 연결을 사용합니다.]]> + 비활성화됨 + 프라이빗 라우팅 사용안함 + 앱 업데이트를 다운로드하는 중입니다. 앱을 닫지 마세요 + %s (%s) 를 다운로드 + 비활성화 + 브라우저를 여는 중 오류 + 차단 + 검은색 + 사용 안 함 (변경 사항 유지) + 자체 소멸 패스코드 활성화 + 데이터베이스 암호화 암호가 업데이트되고 설정에 저장됩니다. + 환영 메시지 입력… + 그룹 생성 + 다크 모드 색상 + 파일 및 미디어가 허용되지 않음 + 향상된 사용자 경험 + 사용자 지정 가능한 메시지 모양. + 최대 200개의 메시지를 삭제하거나 관리할 수 있습니다. + 링크 세부 정보를 다운로드하는 중 + 암호 해독 오류 + 다운로드한 파일 + 중복 + 암호를 확인하는 중 오류: + 그룹 전송 알림을 활성화하시겠습니까? + %d 그룹 이벤트 + 보낸 날짜 + 전송 디버그 + 파일 및 미디어는 이 그룹에서 금지됩니다. + 이 기기의 이름을 입력하십시오… + 데스크톱을 찾음 + 전달 서버: %1$s\n대상 서버 오류: %2$s + 전달 서버: %1$s\n오류: %2$s + %1$s 메시지를 전송하시겠습니까? + 파일 없이 메시지를 전달하시겠습니까? + %1$s 메시지 전송 중 + 메시지 전송… + Android 설정에서 이 권한을 찾아 수동으로 허용하십시오. + 글꼴 크기 + 안녕하세요! + 안녕하세요! + 파일 서버 오류: %1$s + 파일이 서버에서 삭제됩니다. + 전달 서버 %1$s가 대상 서버 %2$s에 연결하지 못했습니다. 나중에 시도하십시오. + 전달 서버 버전이 네트워크 설정과 호환되지 않습니다: %1$s. + 전달 서버 주소가 네트워크 설정과 호환되지 않습니다: %1$s. + 전송됨 + 에서 전송됨 + 파일 상태: %s + 맞춤 + 드디어, 우리는 그것들을 얻었냈습니다! 🚀 + 파일이 삭제되었거나 링크가 유효하지 않음 + 이전 완료 + 전송 + 전송됨 + 메시지 전송… + 연락처에서 지원하지 않는 수정 + 채우기 + 수정 + 연결 수정 + 연결을 수정하시겠습니까? + 그룹 멤버에서 지원하지 않는 수정 + 메시지 전송 및 저장 + 배터리 사용량을 더욱 줄임 + 한 번에 최대 20개의 메시지를 전달할 수 있습니다. + 다른 기기로 이전을 완료합니다. + 파일 상태 + 프랑스어 인터페이스 + 메시지 전송 경고 + 종단 간 암호화로 보호되며, 완벽한 전방 비밀성, 부인 방지 및 침입 복구 기능이 포함되어 있습니다.]]> + 양자 저항 종단 간 암호화로 보호되며, 완벽한 전방 비밀성, 메시지 부인 방지 및 침입 복구 기능이 포함되어 있습니다.]]> + 보관함을 가져오는 중 + 메시지를 선택한 후 메시지가 삭제되었습니다. + 메시지 + 권한 부여 + 설정에서 부여 + 전화 권한 부여 + 헤드폰 + 메시지 초안 + 앱을 열 때 자체 소멸 패스코드를 입력하는 경우: + 메시지 초안 + 향상된 메시지 전송 + 메시지 출처는 비공개로 유지됩니다. + 호환되지 않는 버전 + 가져오기 실패 + 수신된 메시지 + 보낸 메시지 + 메시지 상태 + 익명 그룹 + 내역 + 정보 + 답장 대상 + 숨기기 + 테마 가져오기 + 메시지 반응 + 이 채팅에서는 메시지 반응이 금지됩니다. + 그룹 관리 + 헝가리어 및 튀르키예어 UI + 향상된 메시지 전송 + 시간 + 글꼴 크기 키우기 + 최근 앱 목록에서 앱 화면을 숨깁니다. + (신규)]]> + 호스트 + 즉시 + 잘못된 패스코드 + 앱을 열 때 이 패스코드를 입력하면 모든 앱 데이터가 되돌릴 수 없게 제거됩니다! + 메시지 수신 + 그룹 멤버는 파일 및 미디어를 보낼 수 있습니다. + 그룹이 이미 존재합니다! + 벨소리 + 메시지 전송됨 + 멤버가 활동 상태가 되면 나중에 메시지가 전달될 수 있습니다. + 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 이미지 + 직접 만날 수 없는 경우 영상 통화에서 QR 코드를 보여주거나 링크를 공유하세요. + 테마 가져오기 오류 + 향상된 익명성 및 보안 + 비활성 + 즉시 알림! + 메시지 반응 + 성공적으로 설치됨 + 업데이트 설치 + 인터페이스 색상 + 메시지 라우팅 대체 + 그룹 멤버가 메시지 반응을 추가할 수 있습니다. + 메시지 모양 + 메시지 대기열 정보 + 메뉴 & 알림 + 그룹 환영 메시지 + 숨겨진 채팅 프로필 + 계속하려면 채팅을 중지시켜야 합니다. + 메시지 상태: %s + %s에서 보낸 메시지가 표시됩니다! + 내역이 새 멤버에게 전송되지 않습니다. + 이 그룹에서는 메시지 반응이 금지됩니다. + 그룹 멤버가 SimpleX 링크를 보낼 수 있습니다. + 향상된 서버 구성 + 메시지 + 메시지 서버 + 메시지 라우팅 모드 + 안녕하세요!\nSimpleX Chat 초대장이 도착했습니다: %s + 내부 오류 + 잘못된 이름입니다! + 유효하지 않은 링크 + 유지 + 유효하지 않은 링크 + 이 문제는 이전 데이터베이스의 백업을 사용하는 경우에 발생할 수 있습니다. + 이탈리아어 인터페이스 + 그룹에 참여하시겠습니까? + k + 그룹 대화에 참여 + (이 기기 v%s)]]> + 잘못된 표시 이름입니다! + 잘못된 파일 경로 + 잘못된 QR 코드 + IP 주소와 연결을 보호합니다. + 초대 + 친구 초대 + 메시지 영구 삭제 + 초대 + 운영자 선택 + %s.]]> + 채팅 프로필 변경 + SOCKS 프록시가 지원하지 않는 경우 .onion 호스트를 No로 사용합니다.]]> + %s 이 찾을 수 없음]]> + 앱 설정에서 앱 배터리 사용량 / 제한 없음 을 선택하세요.]]> + %s 이 연결 끊김]]> + %1$s!]]> + 사용해서는 안 됩니다.]]> + 사용자 가이드에서 확인하세요.]]> + 모바일 앱에서 열기 버튼을 클릭합니다.]]> + 운영자 + %s.]]> + 약관 수락 날짜: %s. + %s.]]> + %s.]]> + %s.]]> + 추가된 미디어 및 파일 서버 + 앱 툴바 + 흐리기 + 약관 수락 + 약관을 수락함 + 추가된 메시지 서버 + 주소 또는 일회용 링크? + 주소 설정 + 한 명의 연락처에만 사용할 수 있으며 - 직접 또는 메신저를 통해 공유하십시오.]]> + %s.]]> + %s.]]> + %s 이 현재 사용 중]]> + %s.]]> + 개인 정보를 보호하기 위해 SimpleX는 푸시 알림을 사용하는 대신 백그라운드에서 실행됩니다.]]> + %s 의 서버를 사용하려면 사용 약관에 동의하십시오.]]> + %1$s 에 연결 중입니다.]]> + 허용을 선택하면 알림을 즉시 받을 수 있습니다.]]> + 앱 설정에서 앱 배터리 사용량 / 제한 없음 을 선택하십시오.]]> + %s 이 현재 비활성화됨]]> + SimpleX Chat 개발자에게 연결하여 질문하고 업데이트를 받을 수 있습니다.]]> + %s 이 연결 끊김]]> + 에서 데스크톱에서 사용을 열고 QR 코드를 스캔합니다.]]> + %s]]> + %1$s 그룹에 가입하는 중 입니다.]]> + %s 버전이 지원되지 않습니다. 두 기기에서 동일한 버전을 사용하는지 확인하십시오.]]> + %1$s 그룹에 속해 있습니다.]]> + 약관을 수락하는 중 오류 발생 + 연결 보안 + 로그 활성화 + 비즈니스 주소 + 일회용 링크 생성 + 10분마다 메시지 확인 + %s:에서 활성화된 운영자에 대한 약관이 자동으로 수락됩니다. + 현재 약관 텍스트를 로드할 수 없습니다, 다음 링크를 통해 약관을 검토할 수 있습니다: + 사용 약관 + Flux 활성화 + 종단 간 암호화로 전송됩니다.]]> + 앱이 항상 백그라운드에서 실행 + 활성화된 운영자에 대한 약관은 30일 후에 수락됩니다. + 팀 멤버 추가하기 + 친구 추가 + 모든 멤버에게서 채팅이 삭제됩니다 - 이 결정은 되돌릴 수 없습니다! + 채팅 삭제 + 채팅을 삭제하시겠습니까? + 채팅 + 서버 추가 중 오류 + 멤버 간의 다이렉트 메시지는 금지됩니다. + Xiaomi 기기: 알림이 작동하려면 시스템 설정에서 자동 시작을 사용하도록 설정하세요.]]> + 이 채팅에서는 멤버 간의 다이렉트 메시지가 금지됩니다. + 당신에게서 채팅이 삭제됩니다 - 이 결정은 되돌릴 수 없습니다! + %s:에서 약관이 수락됩니다. + %1$s에 연결되어 있습니다.]]> + 채팅이 이미 존재합니다! + 계속 + 수락된 초대 + 비즈니스 채팅 + 운영자 소개 + 연결에 암호화 재협상이 필요합니다. + 암호화 재협상이 진행 중입니다. + 팀 멤버를 대화에 추가하세요. + 다른 이유 + 보관 + 1 보고서 + 리스트 추가 + 전부 + 목록 %s의 모든 차트가 제거되었고, 목록도 삭제되었습니다. + 1 년 + 리스트에 추가하기 + 보관된 보고서 + 보관된 신고: %s + 커뮤니티 가이드라인 위반 + 신고를 보관하시겠습니까? + 신고 보관 + 연락처 + 연결이 차단되었습니다 + 연결이 서버 운영자에 의해 차단되었습니다:\n%1$s + 비즈니스 + 순서 변경 + 문의 + 연결이 준비되지 않았습니다. + 자동 메시지 삭제를 변경하시겠습니까? + 그룹 성능 향상 + 모든 신고가 보관됩니다. + 모든 신고를 보관하시겠습니까? + 신고 보관 + 목록 변경 + 메시지를 운영자에게 신고할 수 있도록 허용합니다. + 더 나은 개인정보 보호 및 보안 + 신고 %d건을 보관하시겠습니까? + 이 표시 이름은 유효하지 않습니다. 다른 이름을 선택하세요. + 데스크톱에 연결하는 동안 시간 초과되었습니다 + 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 43237fa2a9..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,7 +6,7 @@ 1 diena a + b Apie SimpleX - Pridėti serverį… + Pridėti serverį Pridėti serverius nuskanuojant QR kodus. Išvaizda Programėlės versija @@ -125,12 +125,9 @@ Failai ir medija Ištrinti visus failus Ištrinti failus ir mediją\? - Ištrinti archyvą - Ištrinti pokalbio archyvą\? grupės profilis atnaujintas Grupė Ištrinti pokalbio profilį\? - Sugrąžinti Įrašyti įjungta Ištrinti po @@ -189,7 +186,6 @@ Daugiau neberodyti Tamsus Atstatyti spalvas - Įrašyti spalvą Grupės parinktys Ištrinti visiems Tiesioginės žinutės @@ -352,7 +348,6 @@ Kas naujo Įrašyti ir pranešti grupės nariams gautas patvirtinimas… - Išsamiau skaitykite mūsų „GitHub“ saugykloje Praleistas skambutis POKALBIAI APIPAVIDALINIMAI @@ -395,7 +390,6 @@ Žymėti kaip patvirtintą SimpleX užraktas Įrašyti WebRTC ICE serveriai bus pašalinti. - Įrašyti archyvą Siųsti tiesioginę žinutę Šalinti narį Šviesus @@ -513,7 +507,7 @@ Išeiti iš grupės\? Nežinoma duomenų bazės klaida: %s Profilis ir ryšiai su serveriu - Tiesioginės žinutės tarp narių šioje grupėje yra uždraustos. + Tiesioginės žinutės tarp narių šioje grupėje yra uždraustos. Garso/vaizdo skambučiai " \nPrieinama versijoje v5.1" @@ -602,7 +596,7 @@ Nepavyko pakviesti kontakto! Pagal pokalbių profilį (numatytieji nustatymai) arba pagal ryšį (BETA). Naudoja daugiau baterijos! Fono paslauga veikia visada - pranešimai rodomi, kai tik atsiranda žinučių.]]> - Negalima pasiekti \"Keystore\", kad išsaugotumėte duomenų bazės slaptažodį + Negalima pasiekti "Keystore", kad išsaugotumėte duomenų bazės slaptažodį Atšaukti failo peržiūrą Atšaukti vaizdo peržiūrą Duomenų bazės ID: %d @@ -836,7 +830,6 @@ Prašome įvesti praeitą slaptažodį po duomenų bazės atsarginės kopijos atstatymo. Šis veiksmas negali būti atšauktas. duomenų bazė naujesnė nei programėlė, bet nėra perkėlimo į senesnę versiją: %s skirtinga migracija programėlėje/duomenų bazėje: %s / %s - Pokalbio archyvas Grupė neaktyvi Gauta Užblokuota administratoriaus @@ -905,7 +898,6 @@ Atsitiktinė slaptafrazė yra saugoma nustatymuose kaip paprastas tekstas. \nJūs galite tai pakeisti vėliau. Pokalbiai veikia - POKALBIO ARCHYVAS Pokalbiai sustabdyti. Jei jau naudojote šią duomenų bazę kitame įrenginyje, turėtumėte perkelti ją atgal prieš pradedant pokalbius. užblokavo %s Keisti gavimo adresą @@ -1063,7 +1055,6 @@ Pokalbiai sustabdyti jungiamasi (priimtas) Kontaktas patikrintas - Sukurta %1$s Duomenų bazės slaptafrazė yra kitokia nei išsaugota raktų saugykloje. Duomenų bazė bus užšifruota ir slaptafrazė bus saugoma nustatymuose. Grupės pakvietimas nebegalioja, siuntėjas jį pašalino. @@ -1228,7 +1219,6 @@ Nustatyti duomenų slaptafrazę Nustatyti slaptafrazę Rodyti paskutines žinutes - Sustabdykite pokalbius, kad įgalinti duomenų bazės veiksmus. PALAIKYKITE SIMPLEX CHAT Jų galima nepaisyti kontaktų ir grupių nustatymuose. Š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. @@ -1374,7 +1364,6 @@ (nuskanuokite ar įklijuokite iš iškarpinės) Priėmėte prisijungimą Jūs pakvietėte kontaktą - Onion serveriai bus reikalingi ryšiui. Onion serveriai bus naudojami, kai tik bus. Išeiti neišsaugant Jūs kontroliuojate savo pokalbį! @@ -1487,7 +1476,7 @@ Nėra pristatymo informacijos Dabar administratoriai gali: \n- ištrinti narių žinutes. -\n- išjungti narius (\"stebėtojas\" rolė) +\n- išjungti narius ("stebėtojas" rolė) įj. Nebemėgti Nustatymų atnaujinimas perjungs klientą iš naujo prie visų serverių. @@ -1540,7 +1529,6 @@ Patvirtinti duomenų bazės slaptafrazę Patvirtinti slaptafrazę Nutildyti - gauti žinutes, jūsų kontaktai - serverius kuriuos naudojate siųsti jiems žinutes.]]> Tarpinis serveris apsaugo jūsų IP adresą, bet jis gali stebėti skambučio trukmę. nėra visapusio šifravimo Naujas duomenų bazės archyvas @@ -1566,7 +1554,6 @@ Jūsų SimpleX adresas Nuskanuoti serverio QR kodą Reikalingi - Onion serveriai bus naudojami, kai tik bus. Onion serveriai nebus naudojami. Savaiminis susinaikinimas Senas duomenų bazės archyvas @@ -1610,7 +1597,6 @@ pakeitėte adresą %s Išplėsti rolių pasirinkimą %1$s.]]> - jei SimpleX neturi naudotojų identifikatorių, kaip jis gali pristatyti žinutes?]]> dviejų sluoksnių visapusiu šifravimu.]]> Kad apsaugoti privatumą, vietoj naudotojų ID naudojamų visose kitose platformose, SimpleX turi identifikatorius žinučių eilėms, skirtingus kiekvienam jūsų kontaktui. Žinutės juodraštis @@ -1629,8 +1615,6 @@ Moderuoti nori prisijungti prie jūsų! nuorodos peržiūros nuotrauka - Onion serveriai nebus naudojami. - Atnaujinti .onion serverių nustatymą? Kai programėlė yra paleista Užrakto režimas Galite paleisti pokalbius per programėlės nustatymus/ duomenų bazę arba paleisdami programėlę iš naujo. @@ -1764,4 +1748,30 @@ SimpleX nuorodos neleidžiamos Kai jungiami garso ir vaizdo skambučiai. Bus įjungta tiesioginiuose pokalbiuose! - \ No newline at end of file + Juodo režimo spalvos + Patvirtinkite failus iš nežinomų serverių. + Paskirties serverio klaida: %1$s + Klaida: %1$s + Tamsu + Spalvos režimas + Programėlės tema + Pridėtinis akcentas 2 + Pažangūs nustatymai + Visi spalvų režimai + Visada + Pokalbio spalvos + Juodas režimas + Juoda + Talpa viršyta – gavėjas negavo anksčiau išsiųstų žinučių. + Pokalbio tema + NESIŲSTI žinučių tiesiogiai, net jei jūsų ar paskirties serveris nepalaiko privataus maršruto. + Visada naudoti privatų maršrutą. + NENAUDOTI privataus maršruto. + Taip + Kopijavimo klaida + Pritaikyti prie + %1$d failo klaida (-os):\n%2$s. + %1$d failas (-ai, -ų) vis dar atsisiunčiamas (-i, -a). + Nepavyko atsisiųsti %1$d failo (-ų). + %d pasirinkta + 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 @@ തിരികെ നിശബ്ദമാക്കുക സഹായം - സെർവർ ചേർക്കുക… + സെർവർ ചേർക്കുക മറ്റൊരു ഉപകരണത്തിലേക്ക് ചേർക്കുക സ്വയമേവ സ്വീകരിക്കുക സ്ഥിരീകരണത്തിനായി കാത്തിരിക്കുന്നു… @@ -280,7 +280,7 @@ ആഴ്ചകൾ അനധികൃത അയക്കുക സ്വീകരിക്കുന്ന വിലാസം മാറണോ\? - കര്‍ത്തവ്യം \"%s\" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും. + കര്‍ത്തവ്യം "%s" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും. സംവിധാനം പ്രാമാണീകരണം അംഗങ്ങളെ ക്ഷണിക്കുന്നത് ഒഴിവാക്കുക സ്വാഗത സന്ദേശം സംരക്ഷിക്കണോ\? @@ -320,10 +320,8 @@ സ്വീകരിക്കുന്ന വിലാസം മാറുക സെർവറുകൾ സംരക്ഷിക്കുക - പഴയപടിയാക്കുക സംവിധാനം സംവിധാനം - നിറം സംരക്ഷിക്കുക ശീർഷകം രണ്ടാംതരമായ അതെ 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 ab10a01a66..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 @@ Toegang tot de servers via SOCKS proxy op poort %d\? De proxy moet worden gestart voordat u deze optie inschakelt. Kan geen contacten uitnodigen! Sta het verzenden van directe berichten naar leden toe. - Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur) + Sta toe om verzonden berichten definitief te verwijderen. (24 uur) Sta toe om spraak berichten te verzenden. Chat is actief Wissen CHAT DATABASE - CHAT ARCHIEF Chat console Chat database geïmporteerd Chat database verwijderd @@ -39,7 +38,7 @@ Accepteer incognito Vooraf ingestelde servers toevoegen Profiel toevoegen - Server toevoegen… + Server toevoegen Toevoegen aan een ander apparaat Beheerders kunnen de uitnodiging links naar groepen aanmaken. Servers toevoegen door QR-codes te scannen. @@ -48,8 +47,7 @@ Toestaan 1 dag Accepteer - Er wordt een afzonderlijke TCP-verbinding (en SOCKS-referentie) gebruikt voor elk contact en groepslid . -\nLet op: als u veel verbindingen heeft, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen uitvallen. + voor elk contact en groepslid.\nLet op: als u veel verbindingen hebt, kan het batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen mislukken.]]> audio oproep Geluid aan Audio en video gesprekken @@ -59,12 +57,12 @@ Contact verzoeken automatisch accepteren vetgedrukt Bijvoegen - Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) + Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) Sta toe om verdwijnende berichten te verzenden. Sta toe dat uw contacten spraak berichten verzenden. Al uw contacten blijven verbonden. Spraak berichten toestaan\? - Goed voor de batterij. Achtergrondservice controleert berichten elke 10 minuten. Mogelijk mist u oproepen of dringende berichten.]]> + Goed voor de batterij. App controleert berichten elke 10 minuten. U kunt oproepen of urgente berichten missen.]]> Onjuiste bericht hash Onjuiste bericht-ID Oproep al beëindigd! @@ -72,11 +70,11 @@ Over SimpleX Over SimpleX Chat hier boven, dan: - Alle gesprekken en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! + Alle chats en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd. Sta verdwijnende berichten alleen toe als uw contact dit toestaat. Sta spraak berichten alleen toe als uw contact ze toestaat. - Laat uw contacten verzonden berichten onomkeerbaar verwijderen. (24 uur) + Laat uw contacten verzonden berichten definitief verwijderen. (24 uur) Sta toe dat uw contacten verdwijnende berichten verzenden. altijd Geluid uit @@ -90,18 +88,18 @@ APP ICON App versie App versie: v%s - voor elk chat profiel dat je in de app hebt .]]> + voor elk chatprofiel dat je in de app hebt .]]> audio oproep (niet e2e versleuteld) Achtergrondservice is altijd actief, meldingen worden weergegeven zodra de berichten beschikbaar zijn. Oproep beëindigd Batterijoptimalisatie is actief, waardoor achtergrondservice en periodieke verzoeken om nieuwe berichten worden uitgeschakeld. Je kunt ze weer inschakelen via instellingen. Het beste voor de batterij. U ontvangt alleen meldingen wanneer de app wordt uitgevoerd (GEEN achtergrondservice).]]> Het kan worden uitgeschakeld via instellingen, meldingen worden nog steeds weergegeven terwijl de app actief is.]]> - Zowel u als uw contact kunnen verzonden berichten onomkeerbaar verwijderen. (24 uur) + Zowel u als uw contact kunnen verzonden berichten definitief verwijderen. (24 uur) Zowel jij als je contact kunnen verdwijnende berichten sturen. Zowel jij als je contact kunnen spraak berichten verzenden. Let op: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijt raakt.]]> - Gebruikt meer batterij! Achtergrondservice wordt altijd uitgevoerd - meldingen worden weergegeven zodra berichten beschikbaar zijn.]]> + Gebruikt meer batterij! App draait altijd op de achtergrond – meldingen worden direct weergegeven.]]> link voorbeeld annuleren oproep beëindigd %1$s Kan de database niet initialiseren @@ -117,12 +115,11 @@ Chat is gestopt Controleert nieuwe berichten elke 10 minuten gedurende maximaal 1 minuut je rol gewijzigd in %s - Gesprek archief Wachtwoord database wijzigen\? Chat is gestopt - Gesprek voorkeuren - Chat profiel - GESPREKKEN + Chat voorkeuren + Chatprofiel + CHATS Praat met de ontwikkelaars Controleer het server adres en probeer het opnieuw. Bestand @@ -180,7 +177,7 @@ "De database wordt versleuteld en het wachtwoord wordt opgeslagen in de Keychain." Het wachtwoord voor database versleuteling wordt bijgewerkt. Database fout - Database wachtwoord is vereist om je gesprekken te openen. + Database wachtwoord is vereist om je chats te openen. Contact bestaat al Oproep verbinden Maak link @@ -215,7 +212,6 @@ Huidige wachtwoord… Database versleuteld! is toegetreden - Gemaakt op %1$s compleet Wissen verbonden @@ -230,10 +226,8 @@ Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken. Donker standaard (%s) - Chat archief verwijderen\? - Archief verwijderen Verwijder contact\? - Chat profiel verwijderen\? + Chatprofiel verwijderen? Verwijderen voor iedereen Link verwijderen direct @@ -250,7 +244,7 @@ Verwijder bericht\? Verwijder berichten Wachtrij verwijderen - Verwijder bestanden voor alle chat profielen + Verwijder bestanden voor alle chatprofielen Verwijder voor mij Groep verwijderen Link verwijderen\? @@ -260,12 +254,12 @@ Apparaatverificatie is uitgeschakeld. SimpleX Vergrendelen uitschakelen. Vul uw naam in: Apparaatverificatie is niet ingeschakeld. Je kunt SimpleX Vergrendelen inschakelen via Instellingen zodra je apparaatverificatie hebt ingeschakeld. - Directe berichten tussen leden zijn verboden in deze groep. + Directe berichten tussen leden zijn niet toegestaan in deze groep. %d bestand(en) met een totale grootte van %s %d uur Uitzetten Verdwijnende berichten - Verdwijnende berichten zijn verboden in dit gesprek. + Verdwijnende berichten zijn niet toegestaan in dit gesprek. SimpleX Vergrendelen uitschakelen Verdwijnende berichten Verbinding verbreken @@ -278,13 +272,13 @@ %ds Verwijder contact Server verwijderen - Verdwijnende berichten zijn verboden in deze groep. + Verdwijnende berichten zijn niet toegestaan. %d sec %dm %dmth %d uren %dh - Chat profiel verwijderen\? + Chatprofiel verwijderen? Chat profiel verwijderen voor verwijderd Beschrijving @@ -344,10 +338,10 @@ ingeschakeld ingeschakeld voor contact voor u ingeschakeld - Groepsleden kunnen verzonden berichten onomkeerbaar verwijderen. (24 uur) - Groepsleden kunnen directe berichten sturen - Groepsleden kunnen spraak berichten verzenden. - Per chat profiel (standaard) of per verbinding (BETA). + Leden kunnen verzonden berichten definitief verwijderen. (24 uur) + Leden kunnen directe berichten sturen. + Leden kunnen spraak berichten verzenden. + Per chatprofiel (standaard) of per verbinding (BETA). Verschillende namen, avatars en transportisolatie. Franse interface Fout bij opslaan van groep profiel @@ -379,7 +373,7 @@ Video Fout bij opslaan van ICE servers geëindigd - Groepsleden kunnen verdwijnende berichten sturen. + Leden kunnen verdwijnende berichten sturen. %d week %dw %d weken @@ -392,7 +386,7 @@ Fout bij opslaan van SMP servers Fout bij updaten van netwerk configuratie Kan het gesprek niet laden - Kan de gesprekken niet laden + Kan de chats niet laden Volledige link dubbel bericht Ongeldige verbinding link @@ -402,7 +396,7 @@ Afbeelding verzonden Live bericht! Als je een uitnodiging link voor SimpleX Chat hebt ontvangen, kun je deze in je browser openen: - Dit kan later worden gewijzigd via instellingen. + Hoe dit de batterij beïnvloedt Deelnemen aan groep\? Nodig leden uit Geen contacten geselecteerd @@ -425,13 +419,12 @@ Hoe te gebruiken Markdown hulp Markdown in berichten - Netwerk instellingen + Geavanceerde instellingen Markdown gebruiken cursief Hoe het werkt gemiste oproep Hoe SimpleX werkt - als SimpleX geen gebruikers-ID\'s heeft, hoe kan het dan berichten bezorgen\?]]> Inkomende audio oproep Inkomend video gesprek Negeren @@ -446,26 +439,26 @@ Groep verlaten Lokale naam Alleen lokale profielgegevens - Het onomkeerbaar verwijderen van berichten is verboden in deze groep. + Het definitief verwijderen van berichten is niet toegestaan. App scherm verbergen in de recente apps. Incognito modus Berichten Nieuw wachtwoord… Keychain fout - Word lid van + Word lid Groep verlaten\? Nieuwe leden rol Geen contacten om toe te voegen - Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chat profiel. + Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel. Licht nee - Meerdere chat profielen + Meerdere chatprofielen Italiaanse interface Concept bericht Meer verbeteringen volgen snel! Nodig leden uit Verberg contact en bericht - op de achtergrond uitvoeren. Anders worden de meldingen uitgeschakeld.]]> + Sta dit toe in het volgende dialoogvenster om direct meldingen te ontvangen.]]> Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld. Onmiddellijk heeft %1$s uitgenodigd @@ -513,9 +506,9 @@ Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn. Als u bevestigt, kunnen de berichten servers en uw provider uw IP-adres zien en met welke servers u verbinding maakt. Nee - Immuun voor spam en misbruik + Immuun voor spam Maak een privéverbinding - Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek. + Het definitief verwijderen van berichten is niet toegestaan in dit gesprek. Nieuw in %s Max 40 seconden, direct ontvangen. Verbeterde serverconfiguratie @@ -546,7 +539,6 @@ Eenmalige uitnodiging link Plakken Vooraf ingesteld server adres - Onion hosts worden niet gebruikt. Privé meldingen Plak de link die je hebt ontvangen Periodiek @@ -560,7 +552,7 @@ aan Alleen jij kunt verdwijnende berichten verzenden. Alleen uw contact kan verdwijnende berichten verzenden. - Alleen u kunt berichten onomkeerbaar verwijderen (uw contact kan ze markeren voor verwijdering). (24 uur) + Alleen u kunt berichten definitief verwijderen (uw contact kan ze markeren voor verwijdering). (24 uur) voorgesteld %s: %2s Oud database archief Voer het juiste huidige wachtwoord in. @@ -573,7 +565,6 @@ Wachtwoord is nodig uit Eenmalige uitnodiging link - Onion hosts zijn vereist voor verbinding. Alleen groep eigenaren kunnen groep voorkeuren wijzigen. (alleen opgeslagen door groepsleden) Vooraf ingestelde server @@ -584,23 +575,22 @@ OK Onion hosts zijn vereist voor verbinding. Onion hosts worden gebruikt indien beschikbaar. - Onion hosts worden gebruikt indien beschikbaar. Onion hosts worden niet gebruikt. - Open-source protocol en code. Iedereen kan de servers draaien. - Mensen kunnen alleen verbinding met u maken via de links die u deelt. + Iedereen kan servers hosten. + Jij bepaalt wie er verbinding mag maken. Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur) Alleen jij kunt spraak berichten verzenden. Alleen uw contact kan spraak berichten verzenden. - Verbied het onomkeerbaar verwijderen van berichten. + Verbied het definitief verwijderen van berichten. voorgesteld %s - Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de gesprekken. + Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chats. Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u deze kwijtraakt. Chat openen Voer het vorige wachtwoord in na het herstellen van de database back-up. Deze actie kan niet ongedaan gemaakt worden. Oproep in behandeling Het openen van de link in de browser kan de privacy en beveiliging van de verbinding verminderen. Niet vertrouwde SimpleX links worden rood weergegeven. Werk de app bij en neem contact op met de ontwikkelaars. - 2-laags end-to-end codering.]]> + Alleen clientapparaten slaan gebruikersprofielen, contacten, groepen en berichten op. De afzender heeft mogelijk het verbindingsverzoek verwijderd. Schakel SimpleX Vergrendelen in om uw informatie te beschermen. \nU wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingeschakeld. @@ -620,15 +610,13 @@ Jij beheert je gesprek! Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen. beginnen… - ontvangt, uw contacten de servers die u gebruikt om ze berichten te sturen.]]> Video aan - Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren. - Deze instelling is van toepassing op berichten in uw huidige chat profiel - Bewaar archief + Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren. + Deze instelling is van toepassing op berichten in uw huidige chatprofiel bijgewerkt groep profiel verwijderd - Uw chat profiel wordt verzonden naar de groepsleden - Je hebt al een chat profiel met dezelfde weergave naam. Kies een andere naam. + Uw chatprofiel wordt verzonden naar de groepsleden + Je hebt al een chatprofiel met dezelfde weergave naam. Kies een andere naam. U bent al verbonden met %1$s. Test mislukt bij stap %s. Veilige wachtrij @@ -649,12 +637,12 @@ verstuurd ongeoorloofd verzenden Ongelezen - Tik om een nieuw gesprek te starten + Tik hier om een nieuw gesprek te starten Deze tekst is beschikbaar in instellingen Welkom! je bent uitgenodigd voor de groep - Je hebt geen gesprekken - Gesprekken + Je hebt geen chats + Chats Deel bestand… Afbeelding delen… Wachten op afbeelding @@ -683,7 +671,7 @@ SimpleX Adres Toon QR-code SimpleX-Logo - Je chat profiel wordt verzonden naar uw contact + Je chatprofiel wordt verzonden naar uw contact Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later! U wordt verbonden wanneer uw verbindingsverzoek wordt geaccepteerd, even geduld a.u.b. of controleer later! Je wordt verbonden wanneer het apparaat van je contact online is, even geduld a.u.b. of controleer het later! @@ -699,14 +687,14 @@ Stuur ons een e-mail SimpleX Vergrendelen SMP servers - Bewaar servers + Servers opslaan Servertest mislukt! Sommige servers hebben de test niet doorstaan: Servers testen Server test Beoordeel de app Gebruik server - Gebruik van SimpleX Chat servers. + Gebruik SimpleX Chat servers. Uw server adres Uw server Transport isolation @@ -722,9 +710,9 @@ U kunt markdown gebruiken voor opmaak in berichten: geweigerde oproep geheim - De volgende generatie privéberichten + De toekomst van berichtenuitwisseling wachten op antwoord… - 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. + Om uw privacy te beschermen, gebruikt SimpleX voor elk van uw contacten afzonderlijke ID\'s. Gebruik chat Wanneer de app actief is video gesprek (niet e2e versleuteld) @@ -749,10 +737,9 @@ CHAT UITVOEREN Uw chat database Wachtwoord instellen om te exporteren - Start de app opnieuw om een nieuw chat profiel aan te maken. + Start de app opnieuw om een nieuw chatprofiel aan te maken. 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. Start de app opnieuw om de geïmporteerde chat database te gebruiken. - Stop de chat om database acties mogelijk te maken. Deze actie kan niet ongedaan worden gemaakt, alle ontvangen en verzonden bestanden en media worden verwijderd. Foto\'s met een lage resolutie blijven behouden. Wachtwoord verwijderen uit Keychain\? Verwijderen @@ -769,13 +756,13 @@ %1$s LEDEN 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. Wijzig - De rol wordt gewijzigd in \"%s\". Iedereen in de groep wordt op de hoogte gebracht. + De rol wordt gewijzigd in "%s". Iedereen in de groep wordt op de hoogte gebracht. Ontvang via Groep profiel opslaan Volledig gedecentraliseerd – alleen zichtbaar voor leden. Timeout van TCP-verbinding Spraak berichten - Spraak berichten zijn verboden in dit gesprek. + Spraak berichten zijn niet toegestaan in dit gesprek. Verbied het verzenden van verdwijnende berichten. Verminderd batterijgebruik Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC. @@ -789,11 +776,11 @@ Onbekende fout Verkeerd wachtwoord! U bent uitgenodigd voor de groep. Word lid om in contact te komen met de groepsleden. - Tik om lid te worden - Tik om incognito lid te worden + Tik hier om lid te worden + Tik hier om incognito lid te worden Je bent lid geworden van deze groep. Verbinding maken met uitnodigend lid. Je hebt een groep uitnodiging verzonden - jij bent vertrokken + je bent vertrokken je bent van adres veranderd Selecteer contacten Sla het uitnodigen van leden over @@ -802,22 +789,20 @@ Lid verwijderen Rol Direct bericht sturen - De rol wordt gewijzigd in \"%s\". De gebruiker ontvangt een nieuwe uitnodiging. + De rol wordt gewijzigd in "%s". De gebruiker ontvangt een nieuwe uitnodiging. Verzenden via SERVERS Resetten naar standaardwaarden Ontvangst adres wijzigen Protocol timeout - Terugdraaien Opslaan sec Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen. Thema Kleuren resetten - Kleur opslaan Systeem ja - gekregen, verboden + gekregen, niet toegestaan Jouw voorkeuren Stel 1 dag in Wat is er nieuw @@ -837,7 +822,7 @@ Spraakbericht… Spraakbericht (%1$s) Contactnaam instellen… - Spraak berichten verboden! + Spraak berichten niet toegestaan! Resetten Verstuur Stuur een live bericht, het wordt bijgewerkt voor de ontvanger(s) terwijl u het typt @@ -846,7 +831,7 @@ Camera Gebruik voor nieuwe verbindingen Star on GitHub - De servers voor nieuwe verbindingen van je huidige chat profiel + De servers voor nieuwe verbindingen van je huidige chatprofiel Uw SMP servers Opgeslagen WebRTC ICE servers worden verwijderd. Uw ICE servers @@ -860,16 +845,16 @@ Transportisolatiemodus updaten\? bevestiging ontvangen… Wachten op bevestiging… - Het eerste platform zonder gebruikers-ID\'s, privé door ontwerp. + Geen gebruikers-ID\'s. Verbied het sturen van directe berichten naar leden. Verbieden het verzenden van spraak berichten. De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits. - Met optioneel welkomst bericht. + Met optioneel welkom bericht. Spraak berichten Uw contacten kunnen volledige verwijdering van berichten toestaan. U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen. Verkeerd wachtwoord voor de database - Bewaar het wachtwoord en open je gesprekken + Wachtwoord opslaan en open je chats De poging om het wachtwoord van de database te wijzigen is niet voltooid. Database back-up terugzetten Database back-up terugzetten\? @@ -899,22 +884,20 @@ Deze string is geen verbinding link! Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren. Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is. - SimpleX achtergrond service - deze gebruikt een paar procent van de batterij per dag.]]> + draait SimpleX op de achtergrond in plaats van pushmeldingen te gebruiken.]]> Jij staat toe Je bent uitgenodigd voor de groep verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen.]]> 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. - .onion hosts-instelling updaten\? SimpleX Chat servers gebruiken\? - Spraak berichten zijn verboden in deze groep. + Spraak berichten zijn niet toegestaan. Welkom %1$s! U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten. je hebt het adres gewijzigd voor %s je hebt %1$s verwijderd Je contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%1$s). - 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. + 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. Je probeert een contact met wie je een incognito profiel hebt gedeeld uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien. U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat. @@ -942,7 +925,6 @@ je hebt een eenmalige link incognito gedeeld Tik op de knop GitHub repository.]]> - Lees meer in onze GitHub repository. %1$d bericht(en) overgeslagen gemodereerd gemodereerd door %s @@ -955,20 +937,20 @@ Initiële rol Waarnemer Je kunt geen berichten versturen! - jij bent waarnemer + je bent waarnemer Systeem Audio en video oproepen Bevestig wachtwoord Chinese en Spaanse interface Voer wachtwoord in bij zoeken Fout bij opslaan gebruikers wachtwoord - Welkomst bericht toevoegen + Welkom bericht toevoegen Niet meer weergeven Groep moderatie Fout bij updaten van gebruikers privacy Verder verminderd batterij verbruik - Groep welkomst bericht - Verborgen chat profielen + Groep welkom bericht + Verborgen chatprofielen Profiel verbergen Verbergen Verborgen profiel wachtwoord @@ -978,23 +960,23 @@ Profiel privé maken! Nu kunnen beheerders: \n- berichten van leden verwijderen. -\n- schakel leden uit (\"waarnemer\" rol) - Bescherm je chat profielen met een wachtwoord! +\n- schakel leden uit ("waarnemer" rol) + Bescherm je chatprofielen met een wachtwoord! Wachtwoord om weer te geven Groep profiel opslaan en bijwerken Servers opslaan\? Bewaar profiel wachtwoord Stel het getoonde bericht in voor nieuwe leden! - Welkomst bericht opslaan\? + Welkom bericht opslaan? Ondersteuning voor bluetooth en andere verbeteringen. - Tik om profiel te activeren. + Tik hier om profiel te activeren. Dank aan de gebruikers – draag bij via Weblate! U kunt een gebruikers profiel verbergen of dempen - houd het vast voor het menu. zichtbaar maken Dempen opheffen - Welkomst bericht - Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chat profielen. - Welkomst bericht + Welkom bericht + Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chatprofielen. + Welkom bericht U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn. Database downgraden Ongeldige migratie bevestiging @@ -1016,9 +998,9 @@ EXPERIMENTEEL Verwijder profiel Profiel wachtwoord - Chat profiel zichtbaar maken + Chatprofiel zichtbaar maken Profiel zichtbaar maken - Chat profiel verwijderen\? + Chatprofiel verwijderen? Gevraagd om de video te ontvangen Er kunnen slechts 10 video\'s tegelijk worden verzonden Te veel video\'s! @@ -1065,7 +1047,7 @@ Je kunt SimpleX Vergrendeling aanzetten via Instellingen. Vergrendeling inschakelen Vergrendeling modus - Indienen + Bevestigen Verificatie geannuleerd Wijzig de vergrendelings modus Bevestig toegangscode @@ -1077,7 +1059,7 @@ Toegangscode ingesteld! Systeem Decodering fout - De hash van het vorige bericht is anders. + De hash van het vorige bericht is anders.\" %1$d berichten overgeslagen. Het kan gebeuren wanneer u of de ander een oude database back-up gebruikt. Meld het alsjeblieft aan de ontwikkelaars. @@ -1105,14 +1087,14 @@ " \nBeschikbaar in v5.1" Audio/video gesprekken verbieden. - Audio/video gesprekken zijn verboden. + Audio/video gesprekken zijn niet toegestaan. Snel en niet wachten tot de afzender online is! App toegangscode Stel het in in plaats van systeemverificatie. Poolse interface Dank aan de gebruikers – draag bij via Weblate! Video\'s en bestanden tot 1 GB - Chat profielen openen + Chatprofielen wijzigen Over SimpleX adres Kom meer te weten Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken. @@ -1136,13 +1118,13 @@ Laten we praten in SimpleX Chat Sla instellingen voor automatisch accepteren op Instellingen opslaan\? - Voer welkomst bericht in... (optioneel) + Voer welkom bericht in... (optioneel) Maak geen adres aan Hoi! \nMaak verbinding met mij via SimpleX Chat: %s U kan het later maken Adres delen - Welkomst bericht invoeren… + Welkom bericht invoeren… Thema importeren SimpleX Extra accent @@ -1165,7 +1147,7 @@ Zorg ervoor dat het bestand de juiste YAML-syntaxis heeft. Exporteer het thema om een voorbeeld te hebben van de themabestandsstructuur. Database openen… Gebruikershandleiding.]]> - THEMA KLEUREN + INTERFACE KLEUREN U kunt uw adres delen als een link of QR-code - iedereen kan verbinding met u maken. Alle app-gegevens worden verwijderd. Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend. @@ -1180,20 +1162,20 @@ Zelfvernietigings wachtwoord ingeschakeld! De app-toegangscode wordt vervangen door een zelfvernietigings wachtwoord. Als u uw zelfvernietigings wachtwoord invoert tijdens het openen van de app: - Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd! + Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens definitief verwijderd! Toegangscode instellen - Berichtreacties verbieden. - Alleen jij kunt berichtreacties toevoegen. - Reacties op berichten zijn verboden in deze groep. + Bericht reacties verbieden. + Alleen jij kunt bericht reacties toevoegen. + Reacties op berichten zijn niet toegestaan. Berichten reacties verbieden. - Sta berichtreacties alleen toe als uw contact dit toestaat. - Sta uw contactpersonen toe om berichtreacties toe te voegen. - Sta berichtreacties toe. - Groepsleden kunnen berichtreacties toevoegen. - Zowel u als uw contact kunnen berichtreacties toevoegen. + Sta bericht reacties alleen toe als uw contact dit toestaat. + Sta uw contactpersonen toe om bericht reacties toe te voegen. + Sta bericht reacties toe. + Leden kunnen reacties op berichten toevoegen. + Zowel u als uw contact kunnen bericht reacties toevoegen. Reacties op berichten - Reacties op berichten zijn verboden in deze chat. - Alleen uw contact kan berichtreacties toevoegen. + Reacties op berichten zijn niet toegestaan in deze chat. + Alleen uw contact kan bericht reacties toevoegen. dagen uren minuten @@ -1243,7 +1225,7 @@ \n- aangepaste tijd om te verdwijnen. \n- bewerkingsgeschiedenis. geen tekst - Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - mogelijk ziet u Chatconsole voor meer informatie. + Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren: Afsluiten\? APP Herstarten @@ -1254,16 +1236,16 @@ Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt. Annuleer het wijzigen van het adres Afbreken - Geen gefilterde gesprekken + Geen gefilterde chats Alleen groep eigenaren kunnen bestanden en media inschakelen. - Bestanden en media zijn verboden in deze groep. + Bestanden en media zijn niet toegestaan. Favoriet - Bestanden en media verboden! + Bestanden en media niet toegestaan! Niet favoriet Bestanden en media Verbied het verzenden van bestanden en media. Sta toe om bestanden en media te verzenden. - Groepsleden kunnen bestanden en media verzenden. + Leden kunnen bestanden en media verzenden. Zoeken Uit Protocol timeout per KB @@ -1308,7 +1290,7 @@ Ontvangst bevestiging verzenden De tweede vink die we gemist hebben! ✅ Filter ongelezen en favoriete chats. - Vind gesprekken sneller + Vind chats sneller Repareer versleuteling na het herstellen van back-ups. Behoud uw verbindingen Eén bericht laten verdwijnen @@ -1415,7 +1397,7 @@ %s, %s en %d leden %1$d berichten gemodereerd door %2$s Verbinding maken met jezelf? - Tik om verbinding te maken + Tik hier om verbinding te maken Juiste naam voor %s? %d berichten verwijderen? Verbinding maken met %1$s? @@ -1466,7 +1448,7 @@ Gekoppelde mobiele apparaten Desktop Verbonden met desktop - Deze apparaatnaam + Naam van dit apparaat Het bestand laden Verbinding maken met desktop Desktop apparaten @@ -1543,14 +1525,14 @@ Zichtbare geschiedenis App toegangscode Nieuw gesprek - Gesprekken laden… + Chats laden… Link maken… Of scan de QR-code Ongeldige QR-code Contact toevoegen - Tik om te scannen + Tik hier om te scannen Bewaar - Tik om de link te plakken + Tik hier om de link te plakken Zoeken of plak een SimpleX link De chat is gestopt. Als u deze database al op een ander apparaat heeft gebruikt, moet u deze terugzetten voordat u met chatten begint. Begin chat? @@ -1614,14 +1596,14 @@ contactadres verwijderd profielfoto verwijderd nieuw contactadres instellen - nieuwe profielfoto instellen + nieuwe profielfoto Geblokkeerd door beheerder Blokkeren voor iedereen Lid voor iedereen blokkeren? Deblokkeer voor iedereen geblokkeerd geblokkeerd door beheerder - %s geblokkeerd + blokkeerde %s %d berichten geblokkeerd door beheerder %s gedeblokkeerd Fout bij blokkeren van lid voor iedereen @@ -1629,7 +1611,7 @@ je hebt %s geblokkeerd je hebt %s gedeblokkeerd Bericht te groot - Welkomstbericht is te lang + Welkom bericht is te lang De databasemigratie wordt uitgevoerd. \nDit kan enkele minuten duren. Audio oproep @@ -1737,10 +1719,10 @@ beheerders alle leden eigenaren - SimpleX-links zijn in deze groep verboden. + SimpleX-links zijn niet toegestaan. Ingeschakeld voor Sta het verzenden van SimpleX-links toe. - Groepsleden kunnen SimpleX-links verzenden. + Leden kunnen SimpleX-links verzenden. opgeslagen opgeslagen van %s Doorsturen @@ -1767,4 +1749,639 @@ Vorm profiel afbeeldingen Profiel afbeeldingen Vierkant, cirkel of iets daartussenin. - \ No newline at end of file + Capaciteit overschreden - ontvanger heeft eerder verzonden berichten niet ontvangen. + Fout met bestemmingsserver: %1$s + Fout: %1$s + Doorstuurserver: %1$s +\nBestemmingsserverfout: %2$s + Doorstuurserver: %1$s +\nFout: %2$s + Waarschuwing voor berichtbezorging + Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden. + Serveradres is niet compatibel met netwerkinstellingen. + Serverversie is incompatibel met netwerkinstellingen. + Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd. + Altijd + Privéroutering + Onbekende servers + Nooit + Onbeschermd + Gebruik altijd privéroutering. + Gebruik privéroutering met onbekende servers. + Gebruik GEEN privéroutering. + Berichtrouteringsmodus + Gebruik privéroutering met onbekende servers wanneer het IP-adres niet beveiligd is. + Downgraden toestaan + Wanneer IP verborgen is + Ja + Nee + Stuur berichten rechtstreeks wanneer uw of de doelserver geen privéroutering ondersteunt. + Toon berichtstatus + Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen. + Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt. + Terugval op berichtroutering + PRIVÉBERICHT ROUTING + Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt. + Onbekende servers! + Zonder Tor of VPN is uw IP-adres zichtbaar voor deze XFTP-relays: +\n%1$s. + Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers. + BESTANDEN + Bescherm het IP-adres + De app vraagt om downloads van onbekende bestandsservers te bevestigen (behalve .onion of wanneer SOCKS-proxy is ingeschakeld). + Fout bij het initialiseren van WebView. Update uw systeem naar de nieuwe versie. Neem contact op met ontwikkelaars. +\nFout: %s + Achtergrond accent + Vullen + Passen + Goedemiddag! + Goedemorgen! + Verwijder afbeelding + Schaal + Alle kleurmodi + Toepassen op + Lichte modus + Laat uw chats er anders uitzien! + Nieuwe chatthema\'s + Routing van privéberichten🚀 + Bevestig bestanden van onbekende servers. + Verbeterde bezorging van berichten + Perzische gebruikersinterface + Veilig bestanden ontvangen + Met verminderd batterijgebruik. + App thema + Terugzetten naar app thema + Terugzetten naar gebruikersthema + Donkere modus + Geavanceerde instellingen + Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen. +\nSchakel dit in in *Netwerk en servers*-instellingen. + Herhalen + Chat kleuren + Profiel thema + Chat thema + Extra accent 2 + Zwart + Kleur mode + Donker + Kleuren in donkere modus + Licht + Antwoord ontvangen + Kleur opnieuw instellen + Antwoord verzonden + Systeem + Wallpaper achtergrond + Stel het standaard thema in + Toon chat-lijst in nieuw venster + geen + Foutopsporing bezorging + Informatie over berichtenwachtrij + informatie over serverwachtrij: %1$s +\n +\nlaatst ontvangen bericht: %2$s + Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd. + Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd. + Bestandsserverfout: %1$s + Bestandsfout + Tijdelijke bestandsfout + Kopieerfout + 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. + Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop. + Bestandsstatus + Berichtstatus: %s + Bestandsstatus: %s + Berichtstatus + Kan bericht niet verzenden + Geselecteerde chat voorkeuren verbieden dit bericht. + Fout in privéroutering + Serverversie is niet compatibel met uw app: %1$s. + Bericht doorgestuurd + Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder. + Overige XFTP servers + Link scannen/plakken + Zoom + Huidig profiel + Bestanden + Server informatie + Informatie weergeven voor + Fouten + Statistieken + Transportsessies + Actieve verbindingen + Details + Berichten ontvangen + Bericht ontvangst + Vanaf %s.\nAlle gegevens blijven privé op uw apparaat. + Verbonden servers + In behandeling + Eerder verbonden servers + Proxied servers + Totaal + Server opnieuw verbinden? + Servers opnieuw verbinden? + Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data. + U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren. + Maak opnieuw verbinding met alle servers + Reset + Reset alle statistieken + Alle statistieken resetten? + Geüpload + Gedetailleerde statistieken + Ontvangen berichten + Verzonden berichten + Totaal verzonden + Proxied + Fouten ontvangen + opnieuw verbinden + Direct verzonden + Verzonden via proxy + SMP server + XFTP server + Erkend + Bevestigingsfouten + Verbindingen + Gemaakt + decoderingsfouten + duplicaten + verlopen + overige fouten + overig + Verzend fouten + Stukken gedownload + Stukken geüpload + Verwijderd + Verwijderingsfouten + Gedownloade bestanden + Beveiligd + Maat + Subscribed + Geüploade bestanden + Upload fouten + Downloadfouten + Server instellingen openen + Server adres + Alle profielen + pogingen + Stukken verwijderd + Voltooid + Verbonden + Verbinden + Fout bij opnieuw verbinding maken met de server + Letter grootte + inactief + Lid inactief + Gedownload + Fout + Fout bij opnieuw verbinden van servers + Fout bij het resetten van statistieken + Het bericht kan later worden bezorgd als het lid actief wordt. + Berichten verzonden + Geen info, probeer opnieuw te laden + Overige SMP servers + Totaal ontvangen + Probeer het later. + Maak opnieuw verbinding met alle verbonden servers om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data. + Het serveradres is niet compatibel met de netwerkinstellingen: %1$s. + Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt! + Beginnend vanaf %s. + Geconfigureerde SMP-servers + Geconfigureerde XFTP-servers + Percentage weergeven + Uitgeschakeld + App update downloaden. Sluit de app niet + %s downloaden (%s) + Succesvol geïnstalleerd + Installeer update + Open de bestandslocatie + Herstart de app. + Herinner later + Sla deze versie over + uitgeschakeld + App update is gedownload + Beta + Controleer op updates + Controleer op updates + Uitschakelen + Subscription fouten + Subscriptions genegeerd + Stabiel + Update beschikbaar: %s + Downloaden van update geannuleerd + Als u op de hoogte wilt worden gehouden van de nieuwe releases, schakelt u periodieke controle op stabiele of bètaversies in. + Vervaag media + Medium + uit + Soft + Krachtig + Het doelserveradres van %1$s is niet compatibel met de instellingen van de doorstuurserver %2$s. + De doelserverversie van %1$s is incompatibel met de doorstuurserver %2$s. + Fout bij verbinden met doorstuurserver %1$s. Probeer het later opnieuw. + Doorstuurserver %1$s kon geen verbinding maken met bestemmingsserver %2$s. Probeer het later opnieuw. + Het doorstuuradres is niet compatibel met de netwerkinstellingen: %1$s. + De doorstuurserverversie is niet compatibel met de netwerkinstellingen: %1$s. + Kan contact niet bellen + Oproepen toestaan? + bellen + Kan geen groepslid bellen + Bellen niet toegestaan! + Contact verwijderen bevestigen? + Gesprek verwijderd! + Verwijderen zonder melding + Gearchiveerde contacten + Contact is verwijderd. + Er wordt verbinding gemaakt met het contact. Even geduld of controleer het later! + Kan geen bericht sturen naar groepslid + %d berichten van leden verwijderen? + Bericht + Berichten worden gemarkeerd voor verwijdering. De ontvanger(s) kunnen deze berichten onthullen. + Niets geselecteerd + verbinden + Contact verwijderd! + Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt! + Behoud het gesprek + bericht + Alleen conversatie verwijderen + open + Plak de link + Uitnodiging + Bereikbare app-toolbars + Vraag uw contactpersoon om oproepen in te schakelen. + Geen gefilterde contacten + Selecteer + Geselecteerd %d + Stuur een bericht om oproepen mogelijk te maken. + zoekopdracht + U kunt berichten naar %1$s sturen vanuit gearchiveerde contacten. + Instellingen + De berichten worden voor alle leden verwijderd. + De berichten worden voor alle leden als gemodereerd gemarkeerd. + video + Je kunt nog steeds het gesprek met %1$s bekijken in de lijst met chats. + U moet uw contactpersoon toestemming geven om te bellen, zodat hij/zij je kan bellen. + Jouw contacten + Berichtservers + Media- en bestandsservers + Chat database geëxporteerd + Doorgaan + SOCKS proxy + U kunt de geëxporteerde database migreren. + U kunt het geëxporteerde archief opslaan. + Opslaan en opnieuw verbinden + TCP verbinding + Het beschermt uw IP-adres en verbindingen. + Toegankelijke chatwerkbalk + Gebruik de app met één hand. + Beheer uw netwerk + Maak sneller verbinding met je vrienden + Archiveer contacten om later te chatten. + Verbindings- en serverstatus. + Verwijder maximaal 20 berichten tegelijk. + Sommige bestanden zijn niet geëxporteerd + Alle hints resetten + Chat-lijst wisselen: + U kunt dit wijzigen in de instellingen onder uiterlijk + Creëren + Vervagen voor betere privacy. + Afspelen via de chat-lijst. + Download nieuwe versies van GitHub. + Vergroot het lettertype. + App automatisch upgraden + Nieuwe chatervaring 🎉 + Nieuwe media-opties + Uitnodigen + Nieuw bericht + Ongeldige link + Controleer of de SimpleX-link correct is. + Fout bij wisselen van profiel + Selecteer chatprofiel + Profiel delen + Uw verbinding is verplaatst naar %s, maar er is een onverwachte fout opgetreden tijdens het omleiden naar het profiel. + CHAT DATABASE + Systeemmodus + Archief verwijderen? + Berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt! + Het geüploade databasearchief wordt permanent van de servers verwijderd. + %1$d bestandsfout(en): +\n%2$s + %1$s berichten niet doorgestuurd + %1$s bericht(en) doorsturen? + Proxy-authenticatie + Berichten zijn verwijderd nadat u ze had geselecteerd. + Gebruikersnaam + %1$d overige bestandsfout(en). + Fout bij het doorsturen van berichten + Niets om door te sturen! + %1$d bestand(en) worden nog gedownload. + %1$d bestand(en) zijn niet gedownload. + Berichten doorsturen zonder bestanden? + %1$d bestand(en) konden niet worden gedownload. + %1$d bestand(en) zijn verwijderd. + Download + %1$s berichten doorsturen + Berichten doorsturen… + %1$s berichten opslaan + Gebruik geen inloggegevens met proxy. + Gebruik voor elke verbinding verschillende proxy-inloggegevens. + Gebruik voor elk profiel verschillende proxy-inloggegevens. + Gebruik willekeurige inloggegevens + Uw inloggegevens worden mogelijk niet-versleuteld verzonden. + Fout bij opslaan proxy + Zorg ervoor dat de proxyconfiguratie correct is. + Wachtwoord + Geluid gedempt + Fout bij initialiseren van WebView. Zorg ervoor dat WebView geïnstalleerd is en de ondersteunde architectuur is arm64.\nFout: %s + Hoek + Berichtvorm + Appsessie + Elke keer dat u de app start, worden er nieuwe SOCKS-inloggegevens gebruikt. + Voor elke server worden nieuwe SOCKS-inloggegevens gebruikt. + Server + Staart + Klik op de infoknop naast het adresveld om het gebruik van de microfoon toe te staan. + Open Safari Instellingen / Websites / Microfoon en kies Toestaan voor localhost. + Als u wilt bellen, geeft u toestemming om uw microfoon te gebruiken. Beëindig het gesprek en probeer opnieuw te bellen. + Betere beveiliging ✅ + SimpleX-protocollen beoordeeld door Trail of Bits. + Betere datums voor berichten. + Betere gebruikerservaring + Aanpasbare berichtvorm. + Wisselen tussen audio en video tijdens het gesprek. + Wijzig chatprofiel voor eenmalige uitnodigingen. + Maximaal 200 berichten verwijderen of modereren. + Stuur maximaal 20 berichten tegelijk door. + Betere gesprekken + Accepteer voorwaarden + Berichtservers toegevoegd + Media- en bestandsservers toegevoegd + Geaccepteerde voorwaarden + Adres of eenmalige link? + %s.]]> + Fout bij het opslaan van servers + Geen berichtenservers. + Geen media- en bestandsservers. + Geen servers om bestanden te ontvangen. + Geen servers om berichten te ontvangen. + Fouten in de serverconfiguratie. + Voor chatprofiel %s: + Niet afgeleverde berichten + De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline. + Dit bericht is verwijderd of nog niet ontvangen. + Tik op SimpleX-adres maken in het menu om het later te maken. + Adres openbaar delen + Deel eenmalig een link met een vriend + Deel het SimpleX-adres op sociale media. + slechts met één contactpersoon worden gebruikt - deel persoonlijk of via een messenger.]]> + Beveiliging van de verbinding + U kunt een verbindingsnaam instellen, zodat u kunt onthouden met wie de link is gedeeld. + SimpleX-adressen en eenmalige links kunnen veilig worden gedeeld via elke messenger. + Nieuwe server + Voor social media + Of om privé te delen + Adres instellingen + Eenmalige link maken + Serverbeheerders + Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd. + Netwerkbeheerders + Later beoordelen + Selecteer welke netwerkoperators u wilt gebruiken. + Update + De app beschermt uw privacy door in elk gesprek verschillende operators te gebruiken. + U kunt operators configureren in Netwerk- en serverinstellingen. + Doorgaan + Voorwaarden bekijken + Uw servers + %s.]]> + %s.]]> + %s.]]> + Voorwaarden geaccepteerd op: %s. + Voorwaarden worden geaccepteerd op: %s. + De tekst van de huidige voorwaarden kon niet worden geladen. U kunt de voorwaarden bekijken via deze link: + Netwerkbeheerder + Operator + %s servers + Gebruik %s + Gebruik servers + Website + %s.]]> + %s.]]> + %s te gebruiken, moet u de gebruiksvoorwaarden accepteren.]]> + Gebruiksvoorwaarden + Voor privé-routering + Wijzigingen openen + De servers voor nieuwe bestanden van uw huidige chatprofiel + Om te ontvangen + Gebruik voor bestanden + Gebruik voor berichten + Bekijk voorwaarden + Serverprotocol gewijzigd. + Operatorserver + Server toegevoegd aan operator %s. + Transparantie + voor betere privacy van metagegevens. + Verbeterde chatnavigatie + Netwerk decentralisatie + De tweede vooraf ingestelde operator in de app! + Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden. + Schakel Flux in bij Netwerk- en serverinstellingen voor betere privacy van metagegevens. + Geen bericht + App-werkbalken + Vervagen + %s.]]> + %s.]]> + Voorwaarden worden automatisch geaccepteerd voor ingeschakelde operators op: %s. + Fout bij het updaten van de server + Fout bij het accepteren van voorwaarden + Fout bij toevoegen server + Geen servers voor het routeren van privéberichten. + Serveroperator gewijzigd. + Geen servers om bestanden te verzenden. + Open voorwaarden + - Open chat op het eerste ongelezen bericht.\n- Ga naar geciteerde berichten. + Vooraf ingestelde servers + Bekijk de bijgewerkte voorwaarden + SimpleX adres of eenmalige link? + Om te voorkomen dat uw link wordt vervangen, kunt u contactbeveiligingscodes vergelijken. + Om te verzenden + U kunt servers configureren via instellingen. + Of importeer archiefbestand + Directe berichten tussen leden zijn in deze chat niet toegestaan. + Externe mobiele telefoons + Xiaomi-apparaten: schakel Automatisch starten in de systeeminstellingen in om meldingen te laten werken.]]> + Bericht is te groot! + Verklein het bericht en verstuur het opnieuw. + U kunt het bericht kopiëren en verkleinen om het te verzenden. + Voeg uw teamleden toe aan de gesprekken. + Zakelijk adres + end-to-end-versleuteld verzonden, met post-kwantumbeveiliging in directe berichten.]]> + App draait altijd op de achtergrond + Controleer berichten elke 10 minuten + Meldingen en batterij + Hoe het de privacy helpt + U ontvangt geen berichten meer van deze chat. De chatgeschiedenis blijft bewaard. + Chat verlaten? + Vrienden toevoegen + Teamleden toevoegen + Uitnodigen voor een chat + De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt! + Chat + Directe berichten tussen leden zijn niet toegestaan. + Chat bestaat al! + Chat verwijderen + Chat verwijderen? + Chat verlaten + Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt! + De rol wordt gewijzigd naar %s. Iedereen in de chat wordt op de hoogte gebracht. + Uw chatprofiel wordt naar chatleden verzonden + Privacy voor uw klanten. + %1$s.]]> + De chat wordt voor alle leden verwijderd - dit kan niet ongedaan worden gemaakt! + Bereikbare chat-toolbar + Zakelijke chats + Geen achtergrondservice + Alleen chateigenaren kunnen voorkeuren wijzigen. + Verklein de berichtgrootte of verwijder de media en verzend het bericht opnieuw. + geaccepteerde uitnodiging + Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert. + verzocht om verbinding te maken + Over operatoren + Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen. + Logs inschakelen + Fout bij het opslaan van de database + Verbinding vereist heronderhandeling over encryptie. + Er wordt opnieuw onderhandeld over de encryptie. + Verbinding herstellen? + Herstel + Verbinding nog niet klaar + Fout bij het bijwerken van de chat-lijst + Geen chats in lijst %s. + Favorieten + Groepen + Geen chats + Geen chats gevonden + Lijst toevoegen + alle + Openen met %s + Maak een lijst + Verwijderen + Lijst verwijderen? + Bewerk + Naam van lijst... + De naam en emoji van de lijst moeten voor alle lijsten verschillend zijn. + Lijst opslaan + Lijst + Fout bij het laden van chat-lijsten + Alle chats worden verwijderd uit de lijst %s, en de lijst wordt verwijderd + Toevoegen aan lijst + bedrijven + Contacten + Fout bij het aanmaken van chat-lijst + Geen ongelezen chats + Notities + Lijst wijzigen + Wijzig volgorde + ‐Fout bij het opslaan van instellingen + Fout bij het rapporteren + Archief + Schending van de communityrichtlijnen + Een andere reden + Rapporteer ledenprofiel: alleen groepsmoderators kunnen dit zien. + moderator + Inhoud melden: alleen groepsmoderators kunnen dit zien. + gearchiveerd rapport + Ongepaste inhoud + Ongepast profiel + Alleen de verzender en moderators zien het + Alleen jij en moderators zien het + Spam + Rapport archiveren? + rapporteren + Reden melding? + Het rapport wordt voor u gearchiveerd. + Anders melden: alleen groepsmoderators kunnen het zien. + Spam melden: alleen groepsmoderators kunnen het zien. + Rapporteer overtreding: alleen groepsmoderators kunnen dit zien. + Rapport archiveren + Rapport verwijderen + gearchiveerd rapport door %s + 1 rapport + %d rapporten + Ledenrapporten + Inhoud schendt de gebruiksvoorwaarden + Verbinding geblokkeerd + Verbinding is geblokkeerd door serveroperator:\n%1$s. + Bestand is geblokkeerd door server operator:\n%1$s. + Nee + Link openen + Open links vIn de chat-lijst + Vragen + Rapporten + Spam + Weblink openen? + Ja + Stel chatnaam in + Automatisch verwijderen van berichten wijzigen? + 1 jaar + Berichten in deze chat zullen nooit worden verwijderd. + Deze actie kan niet ongedaan worden gemaakt. De berichten die eerder in deze chat zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. + Automatisch verwijderen van berichten uitschakelen? + Verwijder chatberichten van uw apparaat. + Berichten verwijderen uitschakelen + standaard (%s) + TCP-poort voor berichtenuitwisseling + Gebruik een webpoort + Gebruik TCP-poort %1$s wanneer er geen poort is opgegeven. + Alles dempen + Ongelezen vermeldingen + Je kunt maximaal %1$s leden per bericht vermelden! + Hiermee kunt u berichten rapporteren aan moderators. + Alle rapporten worden voor u gearchiveerd. + Alle rapporten archiveren? + %d rapporten archiveren? + Voor mij + Leden kunnen berichten melden bij moderators. + Het melden van berichten in deze groep is niet toegestaan + Rapporteer: %s + Het melden van berichten aan moderators is niet toegestaan. + Voor alle moderators + Rapporten archiveren + Betere prestaties van groepen + Stel de berichtvervaldatum in chats in. + Betere privacy en veiligheid + Mis geen belangrijke berichten. + Sneller verwijderen van groepen. + Ontvang een melding als u vermeld wordt. + Help beheerders bij het modereren van hun groepen. + Vermeld leden 👋 + Organiseer chats in lijsten + Namen van persoonlijke mediabestanden. + Rapporteer privé + Sneller verzenden van berichten. + afgewezen + afgewezen + Fout bij het lezen van database wachtwoord + Alle nieuwe berichten van deze leden worden verborgen! + Leden voor iedereen blokkeren? + Leden worden uit de chat verwijderd. Dit kan niet ongedaan worden gemaakt! + Leden worden uit de groep verwijderd. Dit kan niet ongedaan worden gemaakt! + Leden voor iedereen deblokkeren? + Berichten van deze leden worden getoond! + moderatoren + 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. + in afwachting van goedkeuring + Leden verwijderen? + Bijgewerkte voorwaarden + 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. + in behandeling + Accepteer + 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. + SimpleX channel link + Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen. + Volledige link + Niet-ondersteunde verbindingslink + Gebruik korte links (BETA) + Korte link + Serveroperators configureren + Privacybeleid en gebruiksvoorwaarden. + Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders. + 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 d667126730..5102c98563 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -181,7 +181,7 @@ jesteś obserwatorem Nie możesz wysyłać wiadomości! Połączony - Obecnie maksymalny obsługiwany rozmiar pliku to %1$s . + Obecnie maksymalny obsługiwany rozmiar pliku to %1$s. Usuń kontakt Usunąć kontakt\? Rozłączony @@ -277,7 +277,7 @@ Zeskanuj kod bezpieczeństwa z aplikacji Twojego kontaktu. Kod bezpieczeństwa Ustawienia - Udostępnij 1-razowy link + Udostępnij link jednorazowy Pokaż kod QR %s jest zweryfikowany Ten kod QR nie jest linkiem! @@ -290,7 +290,7 @@ Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później! Zostaniesz połączony, gdy urządzenie Twojego kontaktu będzie online, proszę czekać lub sprawdzić później! Dodaj gotowe serwery - Dodaj serwer… + Dodaj serwer Dodaj do innego urządzenia Konsola czatu Sprawdź adres serwera i spróbuj ponownie. @@ -335,8 +335,7 @@ Wersja aplikacji Wersja aplikacji: v%s dla każdego profilu czatu, który masz w aplikacji.]]> - Oddzielne połączenie TCP (i poświadczenia SOCKS) będą używane dla każdego kontaktu i członka grupy. -\nUwaga: 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ć. + dla każdego kontaktu i członka grupy. \nUwaga: 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ć.]]> Profil czatu Połączenie Wersja rdzenia: v%s @@ -344,18 +343,15 @@ Serwery ICE (po jednym na linię) Jeśli potwierdzisz, serwery wiadomości będą mogły zobaczyć Twój adres IP, a Twój dostawca - z jakimi serwerami się łączysz. Sieć i serwery - Ustawienia sieci + Zaawansowane ustawienia Nie Hosty onion będą wymagane do połączenia. \nUwaga: nie będziesz mógł połączyć się z serwerami bez adresu .onion. - Hosty onion będą wymagane do połączenia. Hosty onion będą używane, gdy będą dostępne. - Hosty onion będą używane, gdy będą dostępne. Hosty onion nie będą używane. Wymagane Zapisz Izolacja transportu - Zaktualizować ustawienie hostów .onion\? Zaktualizować tryb izolacji transportu\? Użyć bezpośredniego połączenia z Internetem\? Użyj hostów .onion @@ -411,20 +407,20 @@ Zdecentralizowane zakończona Jak korzystać z markdown - Odporność na spam i nadużycia + Odporność na spam kursywa nieodebrane połączenie - Otwarto źródłowy protokół i kod - każdy może uruchomić serwery. - Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz. + Każdy może hostować serwery. + Ty decydujesz, kto może się połączyć. Redefinicja prywatności otrzymano odpowiedź… otrzymano potwierdzenie… odrzucone połączenie sekret uruchamianie… - strajk - Pierwsza platforma bez żadnych identyfikatorów użytkowników – z założenia prywatna. - Następna generacja prywatnych wiadomości + przekreślenie + Brak identyfikatorów użytkownika. + Następna generacja \nprywatnych wiadomości oczekiwanie na odpowiedź… oczekiwanie na potwierdzenie… Możesz używać markdown do formatowania wiadomości: @@ -434,16 +430,13 @@ Natychmiastowy Można to później zmienić w ustawieniach. Nawiąż prywatne połączenie - jeśli SimpleX nie ma identyfikatora użytkownika, jak może dostarczać wiadomości\?]]> dwuwarstwowego szyfrowania end-to-end.]]> Okresowo Prywatne powiadomienia repozytorium GitHub.]]> - Przeczytaj więcej na naszym repozytorium GitHub. Użyj czatu Gdy aplikacja jest uruchomiona - odbierać wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości.]]> - Zużywa więcej baterii! Usługa zawsze działa w tle - powiadomienia są wyświetlane, gdy tylko wiadomości są dostępne.]]> + Zużywa więcej baterii! Aplikacja zawsze działa w tle - powiadomienia są wyświetlane natychmiastowo.]]> Przychodzące połączenie audio Przychodzące połączenie wideo Wklej link, który otrzymałeś @@ -569,7 +562,6 @@ Uruchom ponownie aplikację, aby utworzyć nowy profil czatu. Zapisz hasło w Keystore %s sekund(y) - Zatrzymaj czat, aby umożliwić działania na bazie danych. 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. Tego działania nie można cofnąć - Twój profil, kontakty, wiadomości i pliki zostaną nieodwracalnie utracone. To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu @@ -603,14 +595,11 @@ Nieprawidłowe hasło bazy danych Nieprawidłowe hasło! Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu. - Archiwum czatu - ARCHIWUM CZATU Czat jest zatrzymany Potwierdź aktualizacje bazy danych Obniż wersję bazy danych Aktualizacja bazy danych wersja bazy danych jest nowsza od aplikacji, ale nie ma migracji w dół dla: %s - Usuń archiwum różne migracje w aplikacji/bazy danych: %s / %s Obniż wersję i otwórz czat Grupa nieaktywna @@ -632,7 +621,6 @@ Przywróć kopię zapasową bazy danych Przywrócić kopię zapasową bazy danych\? Błąd przywracania bazy danych - Zapisz archiwum Próba zmiany hasła bazy danych nie została zakończona. Ta grupa już nie istnieje. Zaktualizuj i otwórz czat @@ -741,7 +729,7 @@ Przełącz Zmień adres odbioru W pełni zdecentralizowana – widoczna tylko dla członków. - Rola zostanie zmieniona na \"%s\". Członek otrzyma nowe zaproszenie. + Rola zostanie zmieniona na "%s". Członek otrzyma nowe zaproszenie. Wiadomość powitalna Usunąć profil czatu\? Usuń profil czatu dla @@ -758,7 +746,6 @@ Profil i połączenia z serwerem Limit czasu protokołu Przywróć wartości domyślne - Przywrócić Zapisz sek Dotknij, aby aktywować profil. @@ -799,7 +786,6 @@ Zabroń wysyłania znikających wiadomości. otrzymane, zabronione Resetuj kolory - Zapisz kolor Ustaw 1 dzień System System @@ -819,7 +805,7 @@ Zarówno Ty, jak i Twój kontakt możecie wysyłać znikające wiadomości. Kontakty mogą oznaczać wiadomości do usunięcia; będziesz mógł je zobaczyć. Usuń po - Bezpośrednie wiadomości między członkami są zabronione w tej grupie. + Bezpośrednie wiadomości między członkami są zabronione w tej grupie. Znikające wiadomości są zabronione na tym czacie. %dm %d min @@ -918,7 +904,7 @@ 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ń. Można je wyłączyć poprzez ustawienia - powiadomienia nadal będą pokazywane podczas działania aplikacji.]]> Najlepsze dla baterii. Będziesz otrzymywać powiadomienia tylko wtedy, gdy aplikacja jest uruchomiona (NIE w tle).]]> - Dobry dla baterii. Usługa w tle sprawdza wiadomości co 10 minut. Możesz przegapić połączenia lub pilne wiadomości.]]> + Dobry dla baterii. Aplikacja sprawdza wiadomości co 10 minut. Możesz przegapić połączenia lub pilne wiadomości.]]> Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe. Według profilu czatu (domyślnie) lub połączenia (BETA). Nie można zaprosić kontaktów! @@ -929,13 +915,11 @@ Kontakt i wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Błąd połączenia (UWIERZYTELNIANIE) Połącz się przez link / kod QR - Utworzony na %1$s Utwórz tajną grupę Utwórz tajną grupę Baza danych jest zaszyfrowana przy użyciu losowego hasła. Proszę zmienić je przed eksportem. %d dni Usuń - Usunąć archiwum czatu\? Usuń wiadomości po Znikające wiadomości są zabronione w tej grupie. Błąd usuwania prośby o kontakt @@ -948,13 +932,12 @@ Nie udało się załadować czatu Teraz administratorzy mogą: \n- usuwać wiadomości członków. -\n- wyłączyć członków (rola \"obserwatora\") +\n- wyłączyć członków (rola "obserwatora")
Z Galerii Obraz Wideo zaproponował %s: %2s Tylko właściciele grup mogą włączyć wiadomości głosowe. - Hosty onion nie będą używane. Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia). (24 godziny) 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. Członkowie grupy mogą wysyłać znikające wiadomości. @@ -998,7 +981,7 @@ Niektóre serwery nie przeszły testu: Dziękujemy za zainstalowanie SimpleX Chat! Wiadomość zostanie oznaczona jako zmoderowana dla wszystkich członków. - Rola zostanie zmieniona na \"%s\". Wszyscy w grupie zostaną powiadomieni. + Rola zostanie zmieniona na "%s". Wszyscy w grupie zostaną powiadomieni. Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną. Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online. Ten link nie jest prawidłowym linkiem połączenia! @@ -1146,15 +1129,15 @@ Podgląd Otwieranie bazy danych… Błąd ustawiania adresu - Otwórz profile czatu + Zmień profile czatu O adresie SimpleX - 1-razowy link + link jednorazowy Podręczniku Użytkownika.]]> Adres SimpleX Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić. Nie stracisz kontaktów, jeśli później usuniesz swój adres. Dostosuj motyw - KOLORY MOTYWU + KOLORY INTERFEJSU Twoje kontakty pozostaną połączone. Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. Utwórz adres, aby ludzie mogli się z Tobą połączyć. @@ -1245,7 +1228,7 @@ \n- niestandardowy czas zniknięcia. \n- historia edycji.
brak tekstu - Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu. + Podczas importu wystąpiły niekrytyczne błędy: Restart APLIKACJA Powiadomienia przestaną działać do momentu ponownego uruchomienia aplikacji. @@ -1390,9 +1373,7 @@ Arabski, bułgarski, fiński, hebrajski, tajski i ukraiński - dzięki użytkownikom i Weblate. Utwórz nowy profil w aplikacji desktopowej. 💻 Przełącz incognito przy połączeniu. - - połącz się z usługą katalogową (BETA)! -\n- potwierdzenia dostaw (do 20 członków). -\n- szybszy i stabilniejszy. + - połącz się z usługą katalogową (BETA)!\n- potwierdzenia dostaw (do 20 członków).\n- szybszy i stabilniejszy. Otwórz Błąd tworzenia kontaktu członka Wyślij wiadomość bezpośrednią aby połączyć @@ -1516,15 +1497,15 @@ Rozłączyć komputer? Proszę poczekać na załadowanie pliku z połączonego telefonu Zweryfikuj połączenie - Odświerz + Odśwież Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach. Losowy błąd wyświetlania zawartości błąd wyświetlania wiadomości - Aby umożliwić aplikacji mobilnej łączenie się z komputerem, otwórz ten port w zaporze sieciowej, jeśli jest ona włączona + Aby umożliwić aplikacji mobilnej połączenie z komputerem stacjonarnym, otwórz ten port w swojej zaporze sieciowej, jeśli jest włączona. Utwórz profil czatu Widok uległ awarii - Otwórz port w zaporze + Otwórz port w zaporze sieciowej Rozłącz telefony Brak połączonych telefonów Historia nie jest wysyłana do nowych członków. @@ -1540,7 +1521,7 @@ Włącz dostęp do kamery Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia. Zachować nieużyte zaproszenie? - Udostępnij ten jednorazowy link + Udostępnij ten jednorazowy link zaproszenia Utwórz grupę: aby utworzyć nową grupę.]]> Widoczna historia Pin aplikacji @@ -1769,4 +1750,438 @@ Kwadrat, okrąg lub cokolwiek pomiędzy. Źródło wiadomości pozostaje prywatne. Zostanie włączone w czatach bezpośrednich! - \ No newline at end of file + Zawsze używaj prywatnego trasowania. + Zezwól na obniżenie wersji + Zawsze + Przekroczono pojemność - odbiorca nie otrzymał wcześniej wysłanych wiadomości. + Błąd serwera docelowego: %1$s + Błąd: %1$s + Serwer przekazujący: %1$s +\nBłąd serwera docelowego: %2$s + Serwer przekazujący: %1$s +\nBłąd: %2$s + Ostrzeżenie dostarczenia wiadomości + Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej. + Adres serwera jest niekompatybilny z ustawieniami sieciowymi. + Wersja serwera jest niekompatybilna z ustawieniami sieciowymi. + Zły klucz lub nieznane połączenie - najprawdopodobniej to połączenie jest usunięte. + Nigdy + Niezabezpieczony + NIE używaj prywatnego trasowania. + Tryb trasowania wiadomości + Tak + Nie + Gdy IP ukryty + Pokaż status wiadomości + TRASOWANIE PRYWATNYCH WIADOMOŚCI + NIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania. + Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości. + Nieznane serwery + Używaj prywatnego trasowania z nieznanymi serwerami. + Rezerwowe trasowania wiadomości + Prywatne trasowanie + Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania. + Wysyłaj wiadomości bezpośrednio, gdy Twój lub docelowy serwer nie obsługuje prywatnego trasowania. + Używaj prywatnego trasowania z nieznanymi serwerami, gdy adres IP nie jest chroniony. + Nieznane serwery! + Bez Tor lub VPN, Twój adres IP będzie widoczny dla tych przekaźników XFTP: +\n%1$s. + Chroń adres IP + Aplikacja będzie prosić o potwierdzenie pobierań z nieznanych serwerów plików (z wyjątkiem .onion lub gdy proxy SOCKS jest włączone). + Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików. + PLIKI + Motyw profilu + Pokaż listę czatów w nowym oknie + Kolory ciemnego trybu + Jasny + Otrzymano odpowiedź + Usuń obraz + Zresetuj kolory + Wypełnij + Dopasuj + Dzień dobry! + Dzień dobry! + Jasny tryb + Powtórz + Dodatkowy akcent 2 + Wszystkie tryby kolorów + Ciemny tryb + Ustaw domyślny motyw + Systemowy + Tło tapety + Zaawansowane ustawienia + Zastosuj dla + Skaluj + Czarny + Akcent tapety + Wyślij odpowiedź + Kolory czatu + Motyw czatu + Tryb koloru + Ciemny + Błąd inicjacji WebView. Zaktualizuj swój system do nowej wersji. Proszę skontaktować się z deweloperami. +\nBłąd: %s + nic + Nowy motywy czatu + Trasowanie prywatnych wiadomości🚀 + Bezpiecznie otrzymuj pliki + Ulepszona dostawa wiadomości + Perski interfejs użytkownika + Potwierdzaj pliki z nieznanych serwerów. + Dostarczenie debugowania + Zrób wygląd Twoich czatów inny! + Chroni Twój adres IP przed przekaźnikami wiadomości wybranych przez Twoje kontakty. +\nWłącz w ustawianiach *Sieć i serwery* . + Informacje kolejki wiadomości + Informacje kolejki serwera: %1$s +\n +\nostatnia otrzymana wiadomość: %2$s + Ze zredukowanym zużyciem baterii. + Motyw aplikacji + Zresetuj do motywu aplikacji + Zresetuj do motywu użytkownika + Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany. + Status pliku + Status wiadomości + Status pliku: %s + Status wiadomości: %s + Kopiuj błąd + Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze. + Błąd pliku + Błąd serwera plików: %1$s + 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. + Tymczasowy błąd pliku + Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty. + Nie można wysłać wiadomości + Wybrane preferencje czatu zabraniają tej wiadomości. + Błąd połączenia z serwerem przekierowania %1$s. Spróbuj ponownie później. + Adres serwera docelowego %1$s jest niekompatybilny z ustawieniami serwera przekazującego %2$s. + Serwer przekazujący %1$s nie mógł połączyć się z serwerem docelowym %2$s. Spróbuj ponownie później. + Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %1$s. + Wersja serwera docelowego %1$s jest niekompatybilna z serwerem przekierowującym %2$s. + Członek nieaktywny + Wiadomość przekazana + Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny. + Beta + Sprawdź aktualizacje + Wyłączony + Pobierz %s (%s) + Aktualizacja aplikacji jest pobrana + Sprawdź aktualizacje + Pobieranie aktualizacji aplikacji, nie zamykaj aplikacji + Zainstalowano pomyślnie + Zainstaluj aktualizacje + Wyłącz + wyłączony + nieaktywny + Rozmiar czcionki + Połączony + Bieżący profil + Otrzymane wiadomości + Odebranie wiadomości + Błąd resetowania statystyk + duplikaty + Zakończono + Połączenia + Utworzono + Błędy usuwania + Fragmenty pobrane + Fragmenty przesłane + Pobrane pliki + Skonfigurowane serwery SMP + Potwierdzono + Błędy potwierdzenia + Usunięto + Aktywne połączenia + Wszystkie profile + Skonfigurowane serwery XFTP + błąd odszyfrowywania + Szczegółowe statystyki + Szczegóły + Błędy pobierania + Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %1$s. + Pliki + Łączenie + Błędy + Połączone serwery + Błąd + Błąd ponownego łączenia z serwerem + Błąd ponownego łączenia serwerów + Pobrane + próby + wygasły + Fragmenty usunięte + Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora. + Inne serwery SMP + Inne serwery XFTP + Pokaż procent + Stabilny + Aktualizacja dostępna: %s + Otwórz lokalizację pliku + Proszę zrestartować aplikację. + Przypomnij później + Pomiń tę wersję + Pobieranie aktualizacji anulowane + Aby otrzymywać powiadomienia o nowych wersjach, włącz okresowe sprawdzanie wersji Stabilnych lub Beta. + Przybliż + Brak informacji, spróbuj przeładować + Informacje o serwerach + Wyświetlanie informacji dla + Statystyki + Sesje transportowe + Zaczynanie od %s. \nWszystkie dane są prywatne na Twoim urządzeniu. + Połącz ponownie wszystkie serwery + Połączyć ponownie serwer? + Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości. + Otrzymane wiadomości + Resetuj + Resetuj wszystkie statystyki + Wysłane wiadomości + Statystyki serwerów zostaną zresetowane - nie można tego cofnąć! + Przesłane + inne + Trasowane przez proxy + Otrzymano łącznie + inne błędy + Zabezpieczone + Zasubskrybowano + Przesłane pliki + Otwórz ustawienia serwera + Adres serwera + Wysłane wiadomości + Ponownie połącz ze wszystkimi połączonymi serwerami w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch. + Zresetować wszystkie statystyki? + Błędy subskrypcji + Subskrypcje zignorowane + Wysłano łącznie + Adres serwera jest niekompatybilny z ustawieniami sieci: %1$s. + Proszę spróbować później. + Błąd prywatnego trasowania + Wersja serwera jest niekompatybilna z aplikacją: %1$s. + Skanuj / Wklej link + Łącznie + Oczekujące + Wcześniej połączone serwery + Serwery trasowane przez proxy + Połączyć ponownie serwery? + Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch. + Wysłano przez proxy + Serwer XFTP + Serwer SMP + Błędy otrzymania + Połącz ponownie + Wysłano bezpośrednio + Zaczynanie od %s. + Wyślij błędy + Rozmiar + Błędy przesłania + Zwiększ rozmiar czcionki. + wiadomość + Wiadomość + otwórz + szukaj + Zaznaczono %d + wideo + Brak filtrowanych kontaktów + Zaproś + Pobieraj nowe wersje z GitHub. + Nowe opcje mediów + Twoje kontakty + Nowa wiadomość + Aktualizuj aplikację automatycznie + Rozmycie dla lepszej prywatności. + Możesz to zmienić w ustawieniach wyglądu. + Zaproś + Nie można wysłać wiadomości do członka grupy + Wyślij wiadomość aby włączyć połączenia. + Nowe możliwości czatu 🎉 + Korzystaj z aplikacji jedną ręką. + Odtwórz z listy czatów. + Zarchiwizowane kontakty + zadzwoń + połącz + Stan połączenia i serwerów. + Utwórz + Nieprawidłowy link + Zapisz i połącz ponownie + Ustawienia + Połączenie TCP + Nic nie jest zaznaczone + Sprawdź czy link SimpleX jest poprawny. + Przełącz listę czatów: + Archiwizuj kontakty aby porozmawiać później. + Chroni Twój adres IP i połączenia. + Osiągalny pasek narzędzi czatu + Silne + Rozmycie mediów + Średni + Wyłącz + Łagodny + Zachowaj rozmowę + Usuń tylko rozmowę + Możesz wysyłać wiadomości do %1$s ze zarchiwizowanych kontaktów. + Nadal możesz przeglądać rozmowę z %1$s na liście czatów. + Potwierdzić usunięcie kontaktu? + Kontakt usunięty! + Kontakt zostanie usunięty – nie można tego cofnąć! + Rozmowa usunięta! + Usuń bez powiadomienia + Wklej link + Zezwolić na połączenia? + Nie można zadzwonić do kontaktu + Łączenie z kontaktem, poczekaj lub sprawdź później! + Kontakt jest usunięty. + Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia. + Połączenia zakazane! + Nie można zadzwonić do członka grupy + Poproś kontakt o włącznie połączeń. + Usunąć %d wiadomości członków? + Wiadomości zostaną oznaczone do usunięcia. Odbiorca(y) będą mogli ujawnić te wiadomości. + Zaznacz + Wiadomości zostaną usunięte dla wszystkich członków. + Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków. + Osiągalny pasek narzędzi czatu + Wyeksportowano bazę danych czatu + Kontynuuj + Serwery mediów i plików + Serwery wiadomości + Proxy SOCKS + Niektóre plik(i) nie zostały wyeksportowane + Możesz zmigrować wyeksportowaną bazy danych. + Możesz zapisać wyeksportowane archiwum. + Zresetuj wszystkie wskazówki + Szybciej łącz się ze znajomymi. + Kontroluj swoją sieć + Usuń do 20 wiadomości na raz. + Błąd przekazywania wiadomości + %1$d plik(ów/i) nie zostały pobrane. + Czy przekazać %1$s wiadomoś(ć/ci)? + Przekazać wiadomości bez plików? + %1$d plik(ów/i) zostały usunięte. + %1$s wiadomoś(ć/ci) nie przekazane + Przekazywanie %1$s wiadomoś(ć/ci) + Przekazywanie wiadomości… + Nie używaj danych logowania do proxy. + Błąd zapisywania ustawień proxy + Sprawdź czy konfiguracja serwera proxy jest poprawna. + %1$d plik(ów/i) dalej są pobierane. + %1$d plik(ów/i) nie udało się pobrać. + Błąd zmiany profilu + BAZA CZATU + %1$d błędów plików:\n%2$s + %1$d innych błędów plików. + Wiadomości zostały usunięte po wybraniu ich. + Nic do przekazania! + Pobierz + Zapisywanie %1$s wiadomości + Uwierzytelnianie proxy + Hasło + Błąd inicjalizacji WebView. Upewnij się, że WebView jest zainstalowany, a jego obsługiwana architektura to arm64.\nBłąd: %s + Wiadomości zostaną usunięte - nie można tego cofnąć! + Usunąć archiwum? + Róg + Kształt wiadomości + Sesja aplikacji + Nowe poświadczenia SOCKS będą używane przy każdym uruchomieniu aplikacji. + Dla każdego serwera zostaną użyte nowe poświadczenia SOCKS. + Kliknij przycisk informacji przy polu adresu, aby zezwolić na korzystanie z mikrofonu. + Otwórz Safari Ustawienia / Strony internetowe / Mikrofon, a następnie wybierz opcję Zezwalaj dla localhost. + Użyj różnych poświadczeń proxy dla każdego połączenia. + Użyj różnych poświadczeń proxy dla każdego profilu. + Użyj losowych poświadczeń + Nazwa użytkownika + Twoje poświadczenia mogą zostać wysłane niezaszyfrowane. + Dźwięk wyciszony + Wybierz profil czatu + Udostępnij profil + Twoje połączenie zostało przeniesione do %s, ale podczas przekierowania do profilu wystąpił nieoczekiwany błąd. + Tryb systemu + Przesłane archiwum bazy danych zostanie trwale usunięte z serwerów. + Serwer + Ogon + Aby wykonywać połączenia, zezwól na korzystanie z mikrofonu. Zakończ połączenie i spróbuj zadzwonić ponownie. + Lepsze bezpieczeństwo ✅ + Lepsze daty wiadomości. + Możliwość dostosowania kształtu wiadomości. + Lepsze połączenia + Lepsze doświadczenie użytkownika + Protokoły SimpleX sprawdzone przez Trail of Bits. + Przełączanie audio i wideo podczas połączenia. + Usuń lub moderuj do 200 wiadomości. + Przekazywanie do 20 wiadomości jednocześnie. + Przełącz profil czatu dla zaproszeń jednorazowych. + Utwórz link jednorazowy + Udostępnij jednorazowy link znajomemu + Adres SimpleX czy link jednorazowy? + Adres lub link jednorazowy? + tylko z jednym kontaktem - udostępnij go osobiście lub przez dowolny komunikator.]]> + Adres SimpleX i jednorazowe linki są bezpieczne do udostępniania przez dowolny komunikator. + raport zarchiwizowany przez %s + Inny powód + Zarchiwizuj raport + Ustawienia adresowe + O operatorach + Adres służbowy + Zaakceptowane warunki + %s.]]> + Dodane serwery wiadomości + %s.]]> + Zarchiwizować raport? + Archiwum + Wszystkie + Biznesy + raport + Zapytaj + %s.]]> + %s.]]> + Rozmyj + Dodaj członków drużyny do konwersacji + Dodaj członków drużyny + Konwersacje służbowe + Dodaj listę + Dodaj do listy + "Wszystkie konwersacje zostaną usunięte z list %s, oraz listy." + zaszyfrowanej, z post-kwantowym bezpieczeństwem w bezpośrednich wiadomościach.]]> + Dodaj znajomych + Dodane serwery plików i mediów + Paski narzędziowe aplikacji + Aplikacja działa zawsze w tle + %s.]]> + %s.]]> + %s.]]> + Zaakceptuj warunki + zaproszenie zaakceptowane + zarchiwizowany raport + Urządzenia Xiaomi: Włącz Autostart w ustawieniach systemowych, aby powiadomienia działały.]]> + Usuń raport + Kontakty + Sprawdzaj wiadomości co 10 minut + Usuń czat + Naruszenie wytycznych społeczności + Warunki zaakceptowane na: %s. + Warunki zostaną automatycznie zaakceptowane dla włączonych operatorów na: %s. + %s.]]> + %s, Zaakceptuj warunki użytkowania.]]> + Warunki użytkowania + Treść narusza warunki użytkowania + Połączenie zablokowane + Połączenie jest zablokowane przez operatora serwera:\n%1$s. + Lista zmian + Utwórz listę + Usuń + Nie można załadować tekstu aktualnych warunków, możesz przejrzeć warunki za pomocą tego linku: + Kontynuuj + Czat + Czat już istnieje! + %1$s.]]> + Usunąć listę? + Połączenie nie jest gotowe. + Usunąć czat? + Czat zostanie dla Ciebie usunięty - nie można tego cofnąć! + Czat zostanie usunięty dla wszystkich członków - nie można tego cofnąć! + Warunki będą akceptowane w dniu: %s. + Bezpieczeństwo połączenia + Warunki będą akceptowane dla operatorów włączonych po 30 dniach. + Zmień kolejność + Połączenie wymaga renegocjacji szyfrowania. + 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 cc0bd49454..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 @@ -57,7 +57,6 @@ Android Keystore é usada para armazenar a senha com segurança - permite que o serviço de notificação funcione. 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. Não é possível acessar a Keystore para salvar a senha do banco de dados - ARQUIVO DE BATE-PAPO O bate-papo está parado Limpar Preferências de bate-papo @@ -74,14 +73,13 @@ Verifique o endereço do servidor e tente novamente. para cada perfil de bate-papo que você tiver no aplicativo.]]> Melhor para bateria. Você receberá notificações apenas quando o aplicativo estiver em execução (SEM o serviço em segundo plano).]]> - Consome mais bateria! O serviço em segundo plano está sempre em execução - as notificações são exibidas assim que as mensagens estiverem disponíveis.]]> + Consome mais bateria! O aplicativo em segundo plano está sempre em execução - as notificações são exibidas instantaneamente.]]> BATE-PAPOS ÍCONE DO APLICATIVO BANCO DE DADOS DE BATE-PAPO O bate-papo está em execução O bate-papo está parado Alterar senha do banco de dados\? - Arquivo de chat endereço alterado para você Você e seu contato podem enviar mensagens temporárias. Backup de dados do aplicativo @@ -91,7 +89,7 @@ O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis. Uma conexão TCP separada (e credencial SOCKS) será usada para cada contato e membro do grupo. \nAtenção: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar. - Bom para bateria. O serviço em segundo plano procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]> + Bom para bateria. O aplicativo procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]> chamda encerrada %1$s Converse com os desenvolvedores Criar link de grupo @@ -110,7 +108,7 @@ Conectar via link/QR code Todas as mensagens serão excluídas - isso não pode ser desfeito! As mensagens serão excluídas APENAS para você. Adicionar servidores pré-definidos - Adicionar servidor… + Adicionar servidor Crie seu perfil Ícone de contexto Contato e todas as mensagens serão excluídas - isso não pode ser desfeito! @@ -180,8 +178,6 @@ Confirmar nova senha… Senha atual… Senha do banco de dados é necessária para abrir o chat. - Excluir arquivo - Excluir arquivo de chat\? cargo alterado de %s para %s conectado Excluir link @@ -218,7 +214,6 @@ Erro de conexão (AUTH) conexão estabelecida conexão %1$d - Criado em %1$s Atualmente, o tamanho máximo de arquivo suportado é %1$s. Excluir Ssnha de criptografia do banco de dados será atualizada e armazenada na Keystore. @@ -256,12 +251,12 @@ DISPOSITIVO Ferramentas de desenvolvedor conectando (introduzido) - Realçe + Tonalidade Erro ao remover membro Erro ao alterar cargo direto erro - Falha ao carregar o bate-papo + Falha ao carregar a conversa Erro ao atualizar a configuração de conexão Erro ao enviar mensagem Erro ao adicionar membro(s) @@ -317,8 +312,8 @@ Expandir seleção de cargo Erro ao salvar o perfil do grupo Mensagens diretas - habilitado - habilitado para contato + ativado + ativado para contato ativado para você %dm %d min @@ -328,7 +323,7 @@ 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. Desativar Desconectado - Mensagens que temporárias são proibidas neste grupo. + Mensagens temporárias são proibidas. Erro ao salvar arquivo O nome de exibição não pode conter espaços em branco. chamada de áudio criptografada ponta-a-ponta @@ -338,7 +333,7 @@ Funcionalidades experimentais Erro ao criar o link de grupo Erro ao excluir o link de grupo - Mensagens diretas entre membros são proibidas neste grupo. + Mensagens diretas entre membros são proibidas neste grupo. %dh %d horas anônimo via link de endereço de contato @@ -346,7 +341,7 @@ Ocultar Da Galeria Vídeo - Os membros do grupo podem enviar mensagens temporárias. + Os membros podem enviar mensagens temporárias. Arquivo Nome completo: Chamada de áudio recebida @@ -365,7 +360,7 @@ Mensagens que desaparecem Preferências do grupo Mensagens temporárias são proibidas nesse bate-papo. - Os membros do grupo podem enviar mensagens diretas. + Os membros podem enviar mensagens diretas. %dmês Link completo Ocultar @@ -379,10 +374,10 @@ Nome completo do grupo: Links de grupo Privacidade e segurança aprimoradas - Falha ao carregar o bate-papo + Falha ao carregar as conversas Arquivo: %s Arquivo salvo - Os membros do grupo podem enviar mensagens de voz. + Os membros podem enviar mensagens de voz. O grupo será excluído para todos os membros - isso não pode ser desfeito! AJUDA Ocultar contato e mensagem @@ -396,14 +391,14 @@ Imagem salva na Galeria anônimo via link do grupo Chamada de vídeo recebida - permite que o SimpleX funcione em segundo plano na próxima caixa de diálogo. Caso contrário, as notificações serão desabilitadas.]]> + Permita na próxima caixa de diálogo para receber notificações instantaneamente]]> Gerar um link de convite de uso único Arquivo não encontrado Se você optar por rejeitar o remetente NÃO será notificado. Código de segurança incorreto! Instale o SimpleX para terminal Como funciona - Imune a spam e abuso + Imune a spam Vire a câmera Desligar Modo anônimo @@ -411,7 +406,7 @@ perfil do grupo atualizado Grupo excluído O modo Incognito protege sua privacidade usando um novo perfil aleatório para cada contato. - Os membros do grupo podem excluir mensagens enviadas de forma irreversível. (24 horas) + Os membros grupo podem excluir mensagens enviadas de forma irreversível. (24 horas) %dsemana Configuração de servidor aprimorada Interface francesa @@ -474,7 +469,6 @@ Compilação do aplicativo: %s Salvar e notificar contato resposta recebida… - Leia mais no nosso repositório do GitHub. Cole o link que você recebeu Quando o aplicativo está em execução Periódico @@ -482,10 +476,8 @@ Arquivo de banco de dados antigo Convidar membros Nenhum contato selecionado - Reverter Salvar Redefinir cores - Salvar cor interface italiana Notificações periódicas Câmera @@ -542,7 +534,7 @@ Suas preferências Definir preferências de grupo Somente você pode excluir irreversivelmente as mensagens (seu contato pode marcá-las para exclusão). (24 horas) - A exclusão irreversível de mensagens é proibida neste grupo. + A exclusão irreversível de mensagens é proibida. Os destinatários vêem as atualizações conforme você as digita. Uso da bateria reduzido Mais melhorias chegarão em breve! @@ -553,18 +545,18 @@ formato de mensagem inválido AO VIVO moderado por %s - chat inválido + conversa inválida 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. Link de conexão inválido Certifique-se de que os endereços do servidor SMP estejam no formato correto, separados por linhas e não estejam duplicados. O remetente pode ter excluído a solicitação de conexão. Notificações periódicas estão desativadas! As notificações instantâneas estão desativadas! - serviço em segundo plano SimpleX - ele usa uma porcentagem da bateria por dia.]]> + SimpleX executa em segundo plano em vez de usar notificações push.]]> Executa quando o aplicativo está aberto enviado o envio falhou - Bate-papos + Conversas Colar Link de convite de uso único Enviar perguntas e idéias @@ -594,9 +586,8 @@ Markdown em mensagens Servidores SMP Endereço do servidor pré-definido - se SimpleX não tem identificadores de usuários, como ele pode mandar mensagens\?]]> Rejeitar - %1$d mensagem(s) ignorada(s) + %1$d mensagem(ens) ignorada(s) Proteger a tela do aplicativo Enviar prévias de links Privacidade e segurança @@ -648,10 +639,9 @@ Onion hosts não serão usados. 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 - Os hosts Onion serão necessários para a conexão. Versão principal: v%s repositório do GitHub.]]> - Pode ser mudado mais tarde via configurações. + Como isso afeta a bateria %1$s quer se conectar com você via sem criptografia ponta-a-ponta Abrir @@ -688,7 +678,6 @@ Esta ação não pode ser desfeita - seu perfil, contatos, mensagens e arquivos serão irreversivelmente perdidos. Remover Senha do banco de dados incorreta - Salvar arquivo 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. Você se juntou a este grupo. Conectando-se a um membro convidado do grupo. Sair do grupo\? @@ -698,14 +687,14 @@ não Agora administradores podem: \n- excluir mensagens de membros. -\n- desativar membros (cargo de \"observador\") +\n- desativar membros (cargo de "observador")
Moderação do grupo Mensagem de boas-vindas do grupo Desatualizar banco de dados migração diferente no aplicativo/banco de dados: %s / %s Convidar para o grupo Sem contatos para adicionar - O cargo será alterada para \"%s\". Todos no grupo serão notificados. + O cargo será alterada para "%s". Todos no grupo serão notificados. Mutar Somente você pode enviar mensagens de voz. Somente seu contato pode enviar mensagens de voz. @@ -762,14 +751,12 @@ Proteja seus perfis de bate-papo com uma senha! Este texto está disponível nas configurações Escanear código - Hosts Onion não serão usados. Os hosts Onion serão usados quando disponíveis. - Os hosts Onion serão usados quando disponíveis. Seu perfil atual Privacidade redefinida Notificações privadas Fazer uma conexão privada - Pessoas podem se conectar com você somente via links compartilhados. + Você decide quem pode se conectar. 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. @@ -790,7 +777,7 @@ Gravar mensagem de voz Certifique-se de que os endereços do servidor WebRTC ICE estão em formato correto, separados por linha e não estejam duplicados. Conexão e servidores - Configurações de conexão + Configurações avançadas sem detalhes Link de convite de uso único Seu endereço de servidor @@ -808,7 +795,7 @@ Não Usar conexão direta com a internet\? Ocultar: - Seu perfil é guardado no seu diapositivo e é compartilhado somente com seus contatos. Servidores SimpleX não podem ver seu perfil. + Seu perfil é guardado no seu dispositivo e é compartilhado somente com seus contatos. Servidores SimpleX não podem ver seu perfil. Salvar preferências\? Salvar e notificar membros do grupo Erro ao salvar a senha do usuário @@ -818,7 +805,7 @@ EXPERIMENTAL você alterou o endereço Atualização do banco de dados - O cargo será alterado para \"%s\". O membro receberá um novo convite. + O cargo será alterado para "%s". O membro receberá um novo convite. Somente os proprietários do grupo podem alterar as preferências do grupo. Adicionar mensagem de boas-vindas Mensagem de boas-vindas @@ -862,11 +849,10 @@ Para começar um novo bate-papo Ligar Bem-vindo(a)! - A próxima geração de mensageiros privados + O futuro da transmissão de mensagens PROXY SOCKS A tentativa de alterar a senha do banco de dados não foi concluída. 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. - Pare o bate-papo para ativar ações no banco de dados. %s segundo(s) Erro de banco de dados desconhecido: %s Erro desconhecido @@ -876,7 +862,7 @@ A atualização das configurações reconectará o cliente a todos os servidores. Atualizar Sistema - Mensagens de voz são proibidas neste grupo. + Mensagens de voz são proibidas. Verificar a segurança da conexão Para proteger o fuso horário, os arquivos de imagem/voz usam UTC. formato de mensagem desconhecido @@ -888,7 +874,7 @@ Inicia periodicamente envio não autorizado Toque para iniciar um novo bate-papo - Você não tem bate-papos + Você não tem conversas aguardando resposta… 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. @@ -899,7 +885,7 @@ você é um observador Mensagem de voz (%1$s) Compartilhar link - 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. + Para proteger a privacidade, SimpleX usa identificadores separados para cada um de seus contatos. chamada de vídeo Mostrar Servidores ICE WebRTC @@ -907,12 +893,12 @@ Atualizar 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. Para receber notificações, por favor, digite a senha do banco de dados - Serviço SimpleX + Serviço de Chat SimpleX Mostrar prévia Mostrar contato e mensagem Mostrar somente contato Compartilhar - Parar bate-papo + Parar conversa Desbloquear A mensagem será excluída para todos os membros. A mensagem será marcada como moderada para todos os membros. @@ -930,7 +916,7 @@ simplexmq: v%s (%2s) Isolamento de transporte Você controla sua conversa! - A 1ª plataforma sem nenhum identificador de usuário – privada por design. + Sem identificadores de usuário. Alto-falante ligado Vídeo desativado Alto-falante desligado @@ -964,7 +950,6 @@ Compartilhar mensagem… Bem-vindo(a) %1$s! você está convidado para o grupo - Atualizar configuração de hosts .onion\? Usar bate-papo Mensagens de voz são proibidas neste chat. Vídeo @@ -982,7 +967,7 @@ iniciando… aguardando confirmação… Não armazenamos nenhum dos seus contatos ou mensagens (uma vez entregues) nos servidores. - Mensagens omitidas + Mensagens ignoradas Toque para ativar o perfil. Mostrar perfil de chat Mostrar perfil @@ -1000,7 +985,7 @@ imagem de pré-visualização do link Para proteger suas informações, ative o bloqueio SimpleX. \nVocê será solicitado a completar a autenticação antes que este recurso seja ativado. - Protocolo de código aberto – qualquer um pode hospedar os servidores. + Qualquer um pode hospedar os servidores. Claro O contato permite ativado @@ -1021,11 +1006,10 @@ APOIE SIMPLEX CHAT 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. Confirme as atualizações do banco de dados - criptografia de ponta a ponta em duas camadas.]]> + Somente o cliente dos dispositivos armazenam perfis de usuários, contatos, grupos e mensagens. Obrigado por instalar o SimpleX Chat! A plataforma de mensagens que protege sua privacidade e segurança. Você está tentando convidar um contato com quem compartilhou um perfil anônimo para o grupo no qual está usando seu perfil principal - receber as mensagens, seus contatos controlam os servidores que você usa para enviar mensagens.]]> Fila segura imagem de perfil temporária Erro ao carregar servidores SMP @@ -1077,8 +1061,8 @@ Senha alterada! Você pode ativar o bloqueio SimpleX via configurações. Hash de mensagem incorreta - O hash da mensagem anterior é diferente. - %1$d descriptografia das mensagens falhou + O hash da mensagem anterior é diferente.\" + %1$d mensagens falharam em serem descriptografadas. ID de mensagem incorreta 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. @@ -1115,7 +1099,7 @@ Revogar Sobre o endereço SimpleX Secundária adicional - Realçe adicional + Tonalidade adicional Link de uso único 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. Crie um endereço para permitir que as pessoas se conectem com você. @@ -1145,7 +1129,7 @@ Você não perderá seus contatos se, posteriormente, excluir seu endereço. Endereço SimpleX Quando as pessoas solicitam uma conexão, você pode aceitá-la ou rejeitá-la. - CORES DO TEMA + CORES DA INTERFACE compartilhar com os contatos A atualização do perfil será enviada aos seus contatos. Salvar configurações\? @@ -1165,7 +1149,7 @@ Digite a mensagem de boas-vindas... (opcional) Salvar configurações de aceitação automática Abrindo banco de dados… - Abrir perfis de bate-papo + Alterar perfis de conversa Compartilhar endereço com os contatos\? Seus contatos continuarão conectados. Todos os dados do aplicativo serão excluídos. @@ -1187,7 +1171,7 @@ Permitir reações à mensagens. Somente você pode adicionar reações à mensagens. Somente seu contato pode adicionar reações à mensagens. - Reações à mensagens são proibidas neste grupo. + Reações a mensagens são proibidas. horas minutos segundos @@ -1215,7 +1199,7 @@ %s (atual) Permitir que seus contatos adicionem reações à mensagens. Você e seu contato podem adicionar reações à mensagens. - Os membros do grupo podem adicionar reações às mensagens. + Os membros podem adicionar reações. Reações à mensagens são proibidas neste bate-papo. Proibir reações à mensagens. personalizado @@ -1244,7 +1228,7 @@ Alterar senha de auto-destruição Se você digitar sua senha de auto-destruição ao abrir o aplicativo: sem texto - Alguns erros não-fatais ocurreram durante importação - pode ver o console de Chat para mais detalhes. + Alguns erros não fatais ocorreram durante importação: Pesquisar Desativado Arquivos e mídia @@ -1265,7 +1249,7 @@ código de segurança alterado Renegociar criptografia %s em %s - Arquivos e mídia são proibidos neste grupo. + Arquivos e mídias são proibidos. Proibir o envio de arquivos e mídia. criptografia OK para %s Correção não suportada pelo membro do grupo @@ -1281,13 +1265,13 @@ Erro ao sincronizar conexão Favorito Arquivos e mídia proibidos! - Os membros do grupo podem enviar arquivos e mídia. + Os membros podem enviar arquivos e mídias. Corrigir Correção não suportada pelo contato Desligar Corrigir conexão Corrigir conexão\? - Sem bate-papo filtrados + Sem conversas filtradas Renegociar Desfavoritar Renegociar a criptografia\? @@ -1341,7 +1325,7 @@ Enviar recibos de entrega serão habilitados para todos os contatos em todos os perfis visíveis. %s e %s conectados Conectar diretamente\? - Nenhum bate-papo selecionado + Nenhuma conversa selecionada Rascunho de mensagem desativado SimpleX não pode ser executado em segundo plano. Você receberá as notificações somente quando o aplicativo estiver em execução. @@ -1568,7 +1552,7 @@ Erro de renegociação de criptografia Erro ao abrir o navegador Erro ao enviar o convite - Carregando bate-papos… + Carregando conversas… %s foi desconectado]]> %s foi desconectado]]> Apenas um dispositivo pode funcionar ao mesmo tempo @@ -1638,4 +1622,764 @@ Criar novo perfil no aplicativo de desktop. 💻 Criptografar arquivos armazenados & arquivos de mídia Novo aplicativo de desktop! - \ No newline at end of file + Arquivando banco de dados + Cancelar migração + Por Favor, note que: 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.]]> + Aplicar + Tema do aplicativo + Preto + Bluetooth + Arquivar e enviar + Aplicar para + Administradores podem bloquear um membro para todos. + Migração de dados do aplicativo + Todos os seus contatos,conversas e arquivos irão ser criptografados seguramente e enviados em partes para relays de XFTP configurados. + Aviso: o arquivo irá ser deletado.]]> + Rede móvel + Sempre + Sempre usar roteamento privado. + Câmera + Câmera e microfone + Permitir o envio de links do SimpleX. + Todos os membros + Configurações avançadas + Verificar atualizações + Completado + Servidores SMP configurados + Servidores XFTP configurados + Verifique sua conexão de internet e tente novamente + Verificar atualizações + Modo de cor + Apagar banco de dados desse dispositivo + O endereço do servidor de destino de %1$s é incompatível com o as configurações %2$s do servidor de encaminhamento. + Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente. + Erro do servidor de destino: %1$s + Inativo + Migre de outro dispositivono novo dispositivo e escaneie o QR code.]]> + Confirme se você se lembra da senha do banco de dados para migrá-lo. + Borrar conteúdo + Borrar para melhor privacidade. + Confirmar exclusão do contato? + conectar + Apagar sem notificar + Tonalidade adicional 2 + Confirmar configurações de rede + Todos os modos de cor + Modo escuro + Não é possível enviar mensagem + Cores do chat + Criar + Confirmar upload + administradores + O contato foi apagado. + Permitir chamadas? + Não é possível chamar o contato + Conectando ao contato, por favor aguarde ou volte depois! + Chamadas proibidas! + Não é possível chamar membro do grupo + Não é possível mandar mensagem para o membro do grupo + Controle sua rede + Apague até 20 mensagens por vez. + Arquivar contatos para conversar depois. + Conecte aos seus amigos mais rapidamente. + Escuro + Confirmar arquivos de servidores desconhecidos. + Copiar erro + Conversa migrada! + chamar + O contato será apagado - essa ação não pode ser desfeita! + Beta + A atualização do aplicativo foi baixada + Tema da conversa + Entrega de depuração + Cores do modo escuro + Criando link de arquivo + Permitir downgrade + Todos os usuários + Conectado + Conectando + Perfil atual + Servidores conectados + Conexões ativas + tentativas + Reconhecido + Erros conhecidos + Conexões + Criado + erros de decriptação + Apagado + Erros de exclusão + Pedaços excluídos + Pedaços baixados + Pedaços carregados + Apagar %d mensagens dos membros? + Contato apagado! + Conversa apagada! + Contatos arquivados + Banco de dados da conversa exportado + Continuar + Conexão e status dos servidores. + Repetir download + Recebendo simultaneidade + Redefinir para o tema do usuário + IU Persa + Aviso de entrega de mensagem + Erro: %1$s + Chave incorreta ou conexão desconhecida - provavelmente esta conexão foi excluída. + Essa conversa é protegida por criptografia ponta a ponta + Repetir upload + Erro de conexão ao servidor de encaminhamento %1$s. Por favor tente mais tarde. + A versão do servidor de encaminhamento é incompatível com as configurações de rede: %1$s. + O servidor de encaminhamento %1$s falhou ao se conectar ao servidor de destino %2$s. Por favor tente mais tarde. + O endereço do servidor de encaminhamento é incompatível com as configurações de rede: %1$s. + A versão do servidor de destino de %1$s é incompatível com o servidor de encaminhamento %2$s. + Problemas de rede - a mensagem expirou após muitas tentativas de envio. + Servidor de encaminhamento: %1$s +\nErro: %2$s + Destinatário(s) não podem ver de onde essa mensagem veio. + A mensagem poderá ser entregue mais tarde se o membro se tornar ativo. + Outros servidores SMP + Para proteger seu endereço IP, roteamento privado usa seus servidores SMP para entregar mensagens. + Pular essa versão + Baixar %s (%s) + Download da atualização cancelado + Mostrar lista de conversas em nova janela + Tamanho da fonte + Fundo do papel de parede + Tonalidade do papel de parede + Remover imagem + Zoom + Definir tema padrão + Proibido enviar links SimpleX + Erro ao carregar o arquivo + Falha ao carregar + Carregando arquivo + Estatísticas detalhadas + Erro no servidor de arquivo: %1$s + Salvo de + Baixar + Encaminhar + Status de arquivo: %s + Convidar + Aumentar tamanho da fonte. + Aprimorar aplicativo automaticamente + Redefinir todas as estatísticas? + As estatísticas dos servidores serão redefinidas - isso não poderá ser desfeito! + Link inválido + Por favor cheque se o link SimpleX está correto + criptografia quantum resistant e2e com perfeito sigilo direto, repúdio e recuperação de vazamento.]]> + Essa conversa é protegida por criptografia quantum resistant ponta a ponta + Por favor tente mais tarde. + Selecionado %d + SimpleX links não permitidos + Arquivos e mídia não permitidos + Mensagem + Erro de arquivo temporário + mensagem + pesquisar + vídeo + Roteamento privado + NÃO use roteamento privado. + Abrir configurações + Fone de ouvido + Erro ao iniciar o WebView. Atualize seu sistema para a nova versão. Por favor contate os desenvolvedores. +\nErro: %s + Desativado + Forte + Ativar em conversas diretas (BETA)! + Finalizar migração em outro dispositivo. + Claro + A origem da mensagem permanece privada. + Migrar aqui + Migrando + Nova experiência de conversa 🎉 + Novas opções de mídia + Ou cole o link do arquivo + Reproduzir da lista de conversa. + Redefinir todas as estatísticas + Aviso: iniciar conversa em múltiplos dispositivos não é suportado e pode causar falhas na entrega de mensagens + Internet cabeada + não deve usar a mesma base de dados em dois dispositivos.]]> + Membros podem enviar links SimpleX. + Importando arquivo + Modo claro + Ativado para + Ao conectar em chamadas de áudio de vídeo. + Migrar dispositivo + Erro ao salvar configurações + Arquivo exportado não existe + %s carregados + Para continuar, a conversa precisa ser interrompida. + Outro + Sem conexão de rede + As preferências de conversa selecionadas proíbem essa mensagem. + Erro de arquivo + Redefinir cor + Sons de chamada + Formato das imagens de perfil + IU Lituana + Roteamento de mensagem privada 🚀 + Novos temas de conversa + Com uso de bateria reduzida. + Falha no download + Falha na importação + Baixando arquivo + Repetir importação + Você pode tentar novamente. + Erro ao exportar banco de dados de conversa + Erro ao verificar a palavra-chave: + salvo + salvo de %s + Encaminhado de + Mensagens de voz não permitidas + Nova mensagem + Colar link + Links SimpleX + Encaminhar e salvar mensagens + Suave + Médio + Você precisa permitir seu contato ligue para poder ligar para ele. + Redefinir para o tema do aplicativo + Resposta recebida + Boa tarde! + Bom dia! + Baixe novas versões no GitHub. + encaminhado + O arquivo foi deletado ou o link está inválido + Abrir tela de migração + Grupos seguros + Preparando upload + Conexão de rede + Servidores desconhecidos! + Sem Tor ou VPN, seu endereço de IP ficará visível para esses relays XFTP +\n%1$s. + Erro ao exibir notificação, contate os desenvolvedores. + Salvo + Encaminhado + Servidores desconhecidos + Desprotegido + Nunca + Modo de roteamento de mensagens + Conceder permissões + Alto falante + Headphones + Sem Tor ou VPN, seu endereço de IP ficará visível para servidores de arquivo. + ARQUIVOS + Fotos de perfil + ROTEAMENTO DE MENSAGEM PRIVADA + criptografia padrão ponta a ponta + proprietários + Migrar para outro dispositivo + Verificar palavra-passe + WiFi + Alternar lista de conversa: + Você pode mudar isso em configurações de Aparência. + desativado + nenhum + informações da fila do servidor: %1$s +\n +\núltima mensagem recebida: %2$s + Por favor peça para seu contato ativar as chamadas. + Enviar mensagem para ativar chamadas. + Salvar e reconectar + Conexão TCP + Barra de ferramentas de conversa acessível + Isso protege seu endereço de IP e conexões. + Use o aplicativo com uma mão. + Arquivos carregados + Baixado + Erro ao redefinir estatísticas + Redefinir + Status de arquivo + Informações da fila de mensagens + Sistema + Repetir + Escala + Preencher + Ajustar + Links SimpleX são proibidos. + Migrar para outro dispositivo via QR code. + Chamadas picture-in-picture + Use o aplicativo enquanto está em chamada. + Será ativado em conversas diretas! + Receber arquivos de forma segura + Gerenciamento de rede + Faça suas conversas terem uma aparência diferente! + Conexão de rede mais confiável. + Preparando download + %s baixados + Colar link de arquivo + Insira a palavra-chave + Erro ao baixar o arquivo + Ou de forma segura compartilhe esse link de arquivo + Verifique a palavra-passe do banco de dados + criptografia ponta-a-ponta com perfeito sigilo direto, repúdio e recuperação de vazamento.]]> + Arquivo não encontrado - provavelmente o arquivo foi excluído ou cancelado. + Chave incorreta ou arquivo de pedaço de endereço - provavelmente o arquivo foi excluído. + Mande mensagens diretamente quando o seu endereço de IP está protegido e o servidor de destino não suporta roteamento privado. + Use roteamento privado em servidores desconhecidos. + Escanear / Colar link + Baixando detalhes de link + Finalizar migração + Criptografia Quantum resistant + Migração concluída + Proteja seu endereço de IP dos retransmissores de mensagem escolhidos por seus contatos. +\nAtive nas configurações *Redes e servidores* . + Iniciar conversa + Configurações + abrir + Manter conversa + Apenas excluir conversa + Outros servidores XFTP + Use roteamento privado em servidores desconhecidos quando o endereço de IP não está protegido. + Sim + Instalar atualização + Atualização disponível: %s + Abrir local do arquivo + Desativar + Microfone + Conceder nas configurações + Encontre essa permissão nas configurações do Android e conceda-a manualmente. + O aplicativo irá perguntar para confirmar os downloads de servidores de arquivo desconhecidos (exceto .onion ou quando o proxy SOCKS estiver habilitado). + Tema de perfil + Defina uma palavra-chave + criptografia quantum resistant e2e + Convidar + Status da mensagem + Entrega de mensagens aprimorada + Quadrado, circulo, ou qualquer coisa entre eles. + 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. + Esse link foi usado em outros dispositivo móvel, por favor crie um novo link no computador. + Erro ao excluir banco de dados + Por favor confirme que as configurações de rede estão corretas para este dispositivo. + Parando conversa + Você pode tentar novamente. + A versão do servidor é incompatível com seu aplicativo: %1$s. + Erro de roteamento privado + O endereço do servidor é incompatível com as configurações de rede: %1$s. + Servidor de encaminhamento: %1$s +\nErro no servidor de destino: %2$s + Endereço do servidor é incompatível com as configurações de rede. + A versão do servidor é incompatível com as configurações de rede. + Encaminhar mensagem… + Quando IP oculto + Proteger endereço IP + Enviar resposta + Arquivos + Não + Baixando atualização do aplicativo, não feche o aplicativo + Conceder permissão para fazer chamadas + Link inválido + Detalhes + Erros + Mensagens recebidas + Mensagens enviadas + Sem informação, tente recarregar + Informação dos servidores + Mostrando informação para + Estatísticas + Sessões de transporte + Recepção de mensagem + Pendente + Começando em %s.\nTodos os dados são mantidos privados em seu dispositivo. + Total + Servidores proxiados + Servidores conectados anteriormente + Reconecte todos os servidores conectados para forçar entrega de mensagem. Isso usa tráfego adicional. + Reconectar servidor? + Reconectar servidores? + Reconectar servidor para forçar entrega de mensagem. Isso usa tráfego adicional. + Você não está conectado nesses servidores. Roteamento privado é usado para entregar mensagens para eles. + Erro + Erro ao reconectar servidor + Erro ao reconectar servidores + Reconectar todos os servidores + Reconectar + Enviar diretamente + Enviar mensagens + Enviar total + Enviar via proxy + Servidor SMP + Mensagens recebidas + Total recebido + Receber erros + Começando de %s. + Servidor XFTP + Seguro + Enviar erros + Inscrito + duplicatas + expirada + outro + Erros de inscrição + outros erros + Proxied + Inscrições ignoradas + Erros de download + Arquivos baixados + Endereço do servidor + Tamanho + Erros de upload + Abrir configurações de servidor + Selecione + As mensagens serão excluídas para todos os membros. + As mensagens serão marcadas como moderadas para todos os membros. + Mensagem encaminhada + Ainda não há conexão direta, a mensagem é encaminhada pelo administrador. + Membro inativo + Nada selecionado + Mensagens serão marcadas para exclusão. O(s) destinatário(s) poderá(ão) revelar essas mensagens. + Você ainda pode ver a conversa com %1$s na lista de conversas. + Nenhum contato filtrado + Seus contatos + NÃO envie mensagens diretamente, mesmo que o seu servidor ou o servidor de destino não suporte roteamento privado. + Mande mensagens diretamente quando o seu servidor ou o servidor de destino não suporta roteamento privado. + Retorno de roteamento de mensagens + Mostrar status da mensagem + Migrar de outro dispositivo + Status da mensagem: %s + Carregado + Servidores de mensagem + Servidores de mídia e arquivo + Mostrar porcentagem + Proxy SOCKS + Você pode salvar o arquivo exportado. + Você pode migrar o banco de dados exportado. + Alguns arquivos não foram exportados + Você pode enviar mensagens para %1$s de Contatos arquivados. + Redefinir todas as dicas + Desativado + Estável + Instalado com sucesso + Por favor reinicie o aplicativo. + Me lembre mais tarde + Para ser notificado sobre os novos lançamentos, habilite a checagem periódica de versões Estáveis e Beta. + Barras de ferramentas de aplicativos acessível + Falha no baixar de %1$d arquivo(s). + %1$s mensagens não encaminhadas. + DADOS DO BATE-PAPO + Utilize credenciais aleatórias + O arquivo de banco de dados enviado será removido permanentemente dos servidores. + Use credenciais diferentes de proxy para cada conexão. + Sua conexão foi movida para %s, mas um erro inesperado ocorreu ao redirecioná-lo para o seu perfil. + %1$d erro(s) de arquivo(s): +\n%2$s + %1$d outro(s) erro(s) de arquivo(s). + Erro ao encaminhar mensagens. + Encaminhar %1$s mensagens? + Encaminhar mensagens sem arquivos? + As mensagens foram excluidas após vocês selecioná-las. + Nada para encaminhar! + %1$d o(s) arquivo(s) ainda está(ão) sendo baixado(s). + %1$d arquivo(s) foi(ram) excluído(s). + %1$d arquivo(s) não foi(ram) baixado(s). + Baixar + Emcaminhar mensagens… + Encaminhando %1$s mensagens. + Salvando %1$s mensagens + Autenticação de proxy + Não utilize credenciais com proxy. + Certifique-se de que configuração do proxy está correta. + Use diferentes credenciais de proxy para cada perfil. + Suas credenciais podem ser enviadas sem criptografia. + Remover arquivo? + As mensagens serão excluídas - isso não pode ser desfeito! + Erro ao alternar perfil + Selecionar perfil de bate-papo + Compartilhar perfil + Modo sistema + Erro ao salvar proxy + Senha + Nome de usuário + Sessão do aplicativo + Endereço ou link de uso único? + Configurações de endereço + Adicione membros da sua equipe às conversas. + Melhores ligações + Servidores de mensagem adicionados + Adicionado servidores de mídia e arquivos + Barra de ferramentas + Aplicativo sempre roda em segundo plano + Adicionar amigos + Condições aceitas + Convite aceito + Adicionar membros da equipe + Sobre operadores + Aceite as condições + denúncia arquivada por %s + Outra razão + Adicionar lista + Todas as conversas serão removidas da lista %s, e a lista será apagada + Melhor segurança ✅ + Arquivar denúncia? + Arquivar denúncia + Todos + Adicionar à lista + Em dispositivos Xiaomi: por favor, ative a opção Autostart nas configurações do sistema para que as notificações funcionem.]]> + Datas de mensagens melhores. + Melhor experiência do usuário + %1$s.]]> + Arquivar + Perguntar + Desfoque + Endereço comercial + denúncia arquivada + Deletar chat + O texto das condições atuais não pôde ser carregado, você pode revisar as condições por meio deste link: + %s.]]> + Formato de mensagem personalizável. + Envio de mensagens mais rápido. + Checar mensagens a cada 10 minutos + Todas novas mensagens destes membros serão ocultadas + Erro ao atualizar servidor + Permitir denunciar mensagens aos moderadores. + Melhorias de privacidade e segurança + Não perca mensagens importantes. + Chat já existente! + Ativar logs + Bloquear membros para todos? + Deletar ou moderar até 200 mensagens. + %s.]]> + %s.]]> + Mensagens diretas entre membros são proibidas neste chat. + Melhor desempenho de grupos + com criptografia de ponta-a-ponta, e com segurança pós-quântica em mensagens diretas.]]> + Chat será deletado para você - essa ação não pode ser desfeita! + Condições aceitas em: %s. + Mensagens diretas entre membros são proibidas. + %s.]]> + Alterar ordem + Erro ao ler a senha do banco de dados + Erro ao aceitar condições + Violação das diretrizes da comunidade + Erro ao salvar servidores + %d denúncias + 1 denúncia + com apenas um contato - compartilhe pessoalmente ou por qualquer aplicativo de mensagens.]]> + Erro ao salvar configurações + Criar link único + Reparar + %s, aceite as condições de uso.]]> + O conteúdo viola as condições de uso + Conexão bloqueada + A conexão está bloqueada pelo operador do servidor:\n%1$s. + O arquivo está bloqueado pelo operador do servidor:\n%1$s. + Deletar denúncia + Empresas + Alterar lista + Continuar + Erro ao salvar banco de dados + %s.]]> + Erro ao inicializar o WebView. Certifique-se de que você tenha o WebView instalado e que sua arquitetura suportada seja arm64.\nErro: %s + Alterar exclusão automática de mensagens? + Desativar exclusão automática de mensagens? + Deletar lista? + 1 ano + padrão (%s) + %s.]]> + %s.]]> + As condições serão aceitas automaticamente para operadores habilitados em: %s. + Segurança de conexão + As condições serão aceitas em: %s. + %s.]]> + Condições de uso + Erro ao adicionar servidor + Chats de empresas + para melhoria da privacidade de metadados. + Exclusão mais rápida de grupos. + Erro ao criar lista de chat + Erro ao carregar lista de chats + Erro ao atualizar a lista de chats + Contatos + Favoritos + Criar lista + Editar + Canto + Ativar o Flux nas Configurações de rede e servidores para melhor privacidade de metadados. + Todas denúncias serão arquivadas para você. + Arquivar todas denúncias? + Arquivar %d denúncias? + Arquivar denúncias + Para todos moderadores + Para mim + Deletar mensagens de chat do seu dispositivo. + Excluir chat? + O chat será deletado para todos os membros - essa ação não pode ser desfeita! + Desativar exclusão de mensagens + Renegociação de criptografia em andamento. + Deletar + Clique no botão de informação perto do campo de endereço para permitir usar o microfone. + As condições serão aceitas para operadores habilitados após 30 dias. + 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. + A conexão não está pronta. + Erros nas configurações de servidores. + Para o perfil de chat %s: + A conexão requer renegociação de criptografia. + Reparar conexão? + Erro ao criar denúncia + Chat + Seus servidores + aprovação pendente + pendente + Os membros podem denunciar mensagens aos moderadores. + Operadores da rede + Operador + Nenhum serviço de segundo plano + Abrir mudanças + moderadores + Descentralização da rede + Privacidade para seus clientes. + Mencione membros 👋 + Seja notificado quando mencionado. + Ajude os administradores a moderar seus grupos. + Organize os chats em listas + Nomes de arquivos de mídia privados. + Conteúdo inapropriado + Perfil inapropriado + Nenhuma mensagem de servidores. + Nenhuma mensagem + Denúncias de membros + Ou compartilhe em particular + Nenhum chat não lido + Nenhum chat + Notas + Abrir com %s + O nome da lista e o emoji devem ser diferentes para todas as listas. + Novas credenciais SOCKS serão usadas para cada servidor. + Notificações e bateria + As mensagens desses membros serão exibidas! + Silenciar tudo + Para redes sociais + Servidores predefinidos + Abrir links da lista de bate-papo + Abrir web link? + Convidar ao chat + Abrir condições + Nome da lista... + Novas credenciais SOCKS serão usadas toda vez que você iniciar o aplicativo. + Abrir link + Forma da mensagem + moderador + A mensagem é muito grande! + Por favor, reduza o tamanho da mensagem e a envie novamente. + Operador da rede + Para roteamento privado + Aprimorada a navegação de bate-papo + - Abra o chat na primeira mensagem não lida.\n- Pule para mensagens citadas. + Os membros serão removidos do chat. Essa ação não pode ser desfeita! + Said do chat + Os membros serão removidos do grupo. Essa ação não pode ser desfeita! + Nove servidor + Nenhum chat encontrado + As mensagens neste chat nunca serão excluídas. + 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. + Somente os proprietários do chat podem alterar as preferências. + 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. + Grupos + Ou importar arquivo compactado + Lista + Reduza o tamanho da mensagem ou remova a mídia e envie novamente. + Como isso ajuda na privacidade + Não + Sair do chat? + O membro será removido do chat - essa ação não pode ser desfeita! + Encaminhe até 20 mensagens de uma vez. + Nenhuma mídia & nenhum arquivo de servidores. + Nenhum servidor para enviar arquivos. + Nenhum servidor para roteamento de mensagens privadas. + Nenhum servidor para receber arquivos. + Nenhum servidor para receber mensagens. + Abra Configurações do Safari / Websites / Microfone, e escolha Permitir para localhost. + Servidor do operador + Nenhum chat na lista %s. + Somente o remetente e os moderadores podem vê-lo. + Somente você e os moderadores podem ver isso + rejeitado + Denunciar + Proibir a denúncia de mensagens aos moderadores. + Denunciar conteúdo: somente os moderadores do grupo poderão ver. + Denunciar perfil de membro: somente moderadores do grupo poderão ver. + Dispositivos móveis remotos + Remover membros? + Denunciar outro: somente os moderadores do grupo poderão ver. + Qual é a razão da denúncia? + Denúncia: %s + rejeitado + É proibido denunciar mensagens neste grupo. + Barra de ferramentas de chat acessível + Denúncias + Operador do servidor alterado. + Definir nome do chat… + Compartilhar o endereço publicamente + Denunciar violação: somente os moderadores do grupo poderão ver. + %s servidores + Protocolos SimpleX analisados pela Trail of Bits. + Enviar denúncias privadas + Defina a expiração de mensagens em chats. + Spam + Spam + Salvar lista + Compartilhe um link único com um amigo + Endereço SimpleX ou link único? + Som silenciado + Alterne entre áudio e vídeo durante a chamada. + Selecione as operadoras de rede a serem utilizadas. + Revisar condições + Endereços SimpleX e links únicos são seguros para compartilhar por meio de qualquer mensageiro. + Operadores do servidor + Servidor adicionado ao operador %s. + riscar + Denunciar spam: somente os moderadores do grupo poderão ver. + Protocolo do servidor alterado. + Compartilhe o endereço do SimpleX nas redes sociais. + Revisar depois + Servidor + O SimpleX Chat e o Flux fizeram um acordo para incluir servidores operados pelo Flux no aplicativo. + conexão solicitada + The role will be changed to %s. Everyone in the chat will be notified. + Transparência + Alterne o perfil de chat para convites únicos. + Desbloquear membros para todos? + Para enviar + Condições atualizadas + O segundo operador predefinido no aplicativo! + Esta mensagem foi excluída ou ainda não foi recebida. + Ver condições atualizadas + Toque em Criar endereço SimpleX no menu para criá-lo mais tarde. + Usar porta TCP %1$s quando nenhuma porta for especificada. + A denúncia será arquivado para você. + Para receber + Esta ação não pode ser desfeita - as mensagens enviadas e recebidas neste chat antes da selecionada serão excluídas. + Para se proteger contra a substituição do seu link, você pode comparar os códigos de segurança dos contatos. + Menções não lidas + Porta TCP para mensagens + Usar porta web + O aplicativo protege sua privacidade usando diferentes operadores em cada conversa. + Quando mais de um operador está ativado, nenhum deles têm metadados para saber quem se comunica com quem. + Sim + Seu perfil de chat será enviado aos membros do chat + Ver condições + Usar para mensagens + Você pode definir o nome da conexão para lembrar com quem o link foi compartilhado. + Você pode configurar servidores nas configurações. + Usar %s + Usar servidores + Website + Você pode mencionar até %1$s membros por mensagem! + Final + Você pode copiar e reduzir o tamanho da mensagem para enviá-la. + Atualização + Você pode configurar operadores em Configurações de rede & servidores. + Usar para arquivos + Você deixará de receber mensagens deste chat. O histórico do chat será preservado. + Para fazer chamadas, permita usar seu microfone. Encerre a chamada e tente ligar novamente. + Os servidores para novos arquivos do seu perfil de chat atual + A conexão atingiu o limite de mensagens não entregues, seu contato pode estar offline. + Mensagens não entregues + Configurar operadores de servidor + Chats privados, grupos e seus contatos não são acessíveis aos operadores de servidor. + Aceitar + Ao usar o SimpleX Chat, você concorda em:\n- enviar apenas conteúdo legal em grupos públicos.\n- respeitar outros usuários – sem spam. + Política de privacidade e condições de uso. + 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 7889ef396e..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 @@ ÍCONE DA APLICAÇÃO 1 mês Mensagens - ARQUIVO DE CONVERSA Adicionar mensagem de boas-vindas Adicional perfil Apenas dados de perfil local Realçar Você permite - Mensagens que desaparecem + Mensagens temporárias sempre não Definir preferências de grupo @@ -49,7 +48,7 @@ Aceitar Permitir que seus contatos enviem mensagens que desaparecem. Definir 1 dia - Permitir a exclusão irreversível de mensagens apenas se o seu contato permitir. + Permitir a exclusão irreversível de mensagens apenas se o seu contato permitir. (24 horas) Permitir que seus contatos eliminem de forma irreversível mensagens enviadas. Permitir mensagens de voz apenas se o contato permitir. Permitir que seus contatos enviem mensagens de voz. @@ -62,12 +61,12 @@ %d min %d mês %d meses - %dmês + %dº mês Administradores podem criar as ligações para entrar em grupos. Mensagens de voz Aceitar automaticamente pedidos de contato Adicionar servidores lendo QR codes. - Mensagens que desaparecem + Mensagens temporárias Mensagens ao vivo As mensagens enviadas serão eliminadas após o tempo definido. Mensagem de rascunho @@ -122,7 +121,7 @@ 1 semana Aceitar aceitar chamada - Adicionar servidor… + Adicionar servidor Aceitar Aceitar pedido de ligação\? Aceitar modo anónimo @@ -130,25 +129,22 @@ Aceder aos servidores via proxy SOCKS no porto %d\? O proxy tem de iniciar antes de ativar esta opção. Adicionar a outro dispositivo administrador - Permitir apagar irreversivelmente as mensagens enviadas. + Permitir apagar irreversivelmente as mensagens enviadas. (24 horas) Eliminar endereço\? Eliminar após Eliminar Eliminar - Arquivo de conversa Eliminar Eliminar todos os ficheiros - Eliminar ficheiro - Eliminar arquivo de conversa\? Eliminar base de dados BASE DE DADOS DE CONVERSA Base de dados de conversa eliminada Nome para Exibição Mostrar: - eliminada + eliminado grupo eliminado Nome do grupo: - O nome para exibição não pode conter espaços em branco. + O nome de exibição não pode conter espaços em branco. %dm Não mostrar novamente Mostrar opções de desenvolvedor @@ -157,9 +153,9 @@ Permitir enviar mensagens que desaparecem. Nome para Exibição: Mostrar código QR - Mensagens que desaparecem são proibidas neste grupo. - Conectar via ligação / código QR - Mensagens que desaparecem são proibidas nesta conversa. + Mensagens temporárias são proibidas neste grupo. + Conectar via link / código QR + Mensagens temporárias são proibidas nesta conversa. Enviar AO VIVO Enviar uma mensagem ao vivo - ela será atualizada para o(s) destinatário(s) à medida que você a digita @@ -198,8 +194,7 @@ Preservar o último rascunho da mensagem, com anexos. Solicitada a recepção do vídeo para cada perfil de conversa que você tiver na aplicação .]]> - Uma conexão TCP separada (e credencial SOCKS) será usada para cada contato e membro do grupo. -\n Por favor note: 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. + para cada contato e membro do grupo. \n Por favor note: 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.]]> Solicitada a recepção da imagem Autenticação indisponível negrito @@ -251,7 +246,7 @@ Base de dados de conversa importada A conversa está parada Verifique o endereço do servidor e tente novamente. - O seu perfil será enviado para o contato do qual você recebeu esta ligação. + O seu perfil será enviado para o contacto do qual você recebeu esta ligação. Erro ao eliminar a base de dados de conversa Erro ao eliminar ligação de grupo Perfil de conversa @@ -270,14 +265,14 @@ Alterar função Alterar a função no grupo\? Erro ao alterar função - O contato permite + O contacto permite Preferências de conversa SimpleX m - Conectar através da ligação de contato\? - Conectar via convite de ligação\? + Conectar através do endereço de contacto? + Conectar via link de convite? Conectar através da ligação do grupo\? - Você irá juntar-se a um grupo ao qual esta ligação se refere e conectar-se aos membros do grupo. + Você irá conectar-se a todos os membros do grupo. erro Erro ao criar perfil! Erro ao adicionar membro(s) @@ -334,7 +329,7 @@ conectando (aceite) conectando (anunciado) conectando - Contato verificado + Contacto verificado Ligação de grupo conectando… conexão estabelecida @@ -348,31 +343,29 @@ Conectado conectando… Tempo limite de conexão - Preferências de contato - Contato escondido: - O contato ainda não está conectado! - Nome do contato + Preferências de contacto + contacto escondido: + O contacto ainda não está conectado! + Nome do contacto Contribuir Copiar Versão principal: v%s - Criado a %1$s Criar ligação de grupo Ligações de grupo conectando chamada… Conectar via ligação Erro de conexão conexão %1$d - O contato já existe + O contacto já existe Convite de ligação de utilização única Salvar Modo anónimo convidado através da ligação do seu grupo - 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 + 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 Conexão 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. - Salvar cor - você partilhou ligação de utilização única - você partilhou ligação anónima de utilização única + você partilhou a ligação de utilização única + você partilhou a ligação anónima de utilização única Ligação de conexão inválida Salvar Se você recebeu convite de ligação do SimpleX Chat, você pode abri-lo no seu navegador: @@ -384,7 +377,6 @@ Salvar e atualizar o perfil do grupo Salvar Salvar senha e abrir conversa - Salvar arquivo Junte-se em modo anónimo Apagar ligação Endereço @@ -398,9 +390,9 @@ Descrição Ligação completa anónimo via ligação de endereço de contato - 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. + 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. Confirmar credenciais - Contato e todas as mensagens serão eliminadas - esta acção não pode ser revertida! + O contacto e todas as mensagens serão eliminadas - esta ação é irreversível! ligação de visualização de imagem Ligação inválida! Guia de Utilizador.]]> @@ -415,12 +407,12 @@ Convite de ligação de utilização única Todos os seus contatos permanecerão conectados. A atualização do perfil será enviada aos seus contatos. 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. - Os contatos podem marcar mensagens para eliminar; você será capaz de as ver. + Os contactos podem marcar mensagens para eliminar; você será capaz de as ver. Criar convite de ligação de utilização única anónimo via ligação de utilização única anónimo via ligação de grupo via ligação de utilização única - Você está a usar um perfil anónimo para este grupo - para impedir a partilha do seu perfil principal não é permitido convidar contatos + Você está a usar um perfil anónimo para este grupo - para impedir a partilha do seu perfil principal, não é permitido convidar contactos Ligação para 1 utilização Criar ligação Apagar ligação\? @@ -434,8 +426,8 @@ Partilhar ligação de utilização única GitHub.]]> Criar perfil - Criar perfil - %d contato(s) selecionado(s) + Criar o seu perfil + %d contacto(s) selecionado(s) Eliminar perfil de conversa para Eliminar para todos %dd @@ -448,16 +440,16 @@ Eliminar ficheiros de todos os perfis de conversa Eliminar mensagens %d ficheiros(s) com tamanho total de %s - Senha atual… - Eliminar conexão pendente\? + Palavra-passe atual… + Eliminar ligação pendente? Eliminar mensagens após Eliminar perfil de conversa\? Eliminar perfil de conversa %d hora %d horas - erro de base de dados - A senha da base de dados é diferente da guardada na Keystore. - A senha da base de dados é necessária para abrir a conversa. + Erro de base de dados + A palavra-passe da base de dados é diferente da armazenada na Keystore. + A palavra-passe da base de dados é necessária para abrir a conversa. Eliminar grupo Eliminar grupo\? direta @@ -465,20 +457,20 @@ Eliminar perfil Criar fila Eliminar para mim - Desabilitar o bloqueio do SimpleX + Desativar o bloqueio do SimpleX Atualmente o tamanho máximo de ficheiro suportado é %1$s. - Eliminar contato + Eliminar contacto Criar grupo secreto Eliminar servidor Personalizar tema Eliminar imagem Não criar endereço - Desabilitar - Senha da base de dados + Desativar + Palavra-passe da base de dados Criar endereço SimpleX ID da base de dados Eliminar ficheiro - Eliminar contato\? + Eliminar contacto? DISPOSITIVO Mensagens diretas Descentralizado @@ -488,19 +480,19 @@ %ds %d seg %dw - Nome para exibição duplicado! + Nome de exibição duplicado! %d segundos Ficheiro guardado Atualização da base de dados %d semana %d semanas - Mensagens diretas entre membros são proibidas neste grupo. + Mensagens diretas entre membros são proibidas neste grupo. Eliminar fila Transferir ficheiro Ficheiro Ficheiro não encontrado Ficheiro - Você não perderá os seus contatos se eliminar o seu endereço mais tarde. + Você não irá perder os seus contatos se eliminar o seu endereço mais tarde. Você pode esconder ou silenciar um perfil de utilizador - pressione-o para o menu. Vídeo ligado Servidores XFTP @@ -526,7 +518,7 @@ Senha da base de dados incorreta esquerda Senha errada! - Você deixará de receber mensagens deste grupo. O histórico de mensagens será preservado. + Você irá deixar de receber mensagens deste grupo. O histórico de mensagens será preservado. Juntar-se ao grupo\? Com mensagem de boas-vindas opcional. via %1$s @@ -559,14 +551,14 @@ Esta ação não pode ser revertida - o seu perfil, contatos, mensagens e ficheiros serão irreversivelmente perdidos. 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. 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. +\nEsta ação é irreversível - o seu perfil, contactos, mensagens e ficheiros serão irreversivelmente perdidos. Marcar como não lido membro MEMBRO Máximo de 40 segundos, recebido instantaneamente. Mais Rede e servidores - Definições de rede + Configurações avançadas EXPERIMENTAL Você pode iniciar a conversa através das Definições da aplicação / Base de Dados ou reiniciando a aplicação. Atualizar @@ -579,7 +571,6 @@ Pré-visualização de notificação Muito provavelmente este contato eliminou a conexão consigo. Este texto está disponível nas definições - Atualizar definições de servidores .onion\? Pode ser alterado mais tarde através das definições. AJUDA SUPORTE SIMPLEX CHAT @@ -592,29 +583,28 @@ Novidades %s Novo pedido de contato Novo arquivo de base de dados - A base de dados está encriptada com uma senha aleatória, você pode alterá-la. + A base de dados está encriptada com uma palavra-passe aleatória, você pode alterá-la. Insira a senha correta. A tentativa de alterar a senha da base de dados não foi concluída. - A sua base de dados de conversas não está encriptada - defina a senha para a proteger. + A sua base de dados de conversas não está encriptada - defina a palavra-passe para a proteger. Insira a senha… - Você tem que inserir a senha sempre que a aplicação é iniciada - ela não é guardada no dispositivo. + Você tem que inserir a palavra-passe sempre que a aplicação é iniciada - ela não é armazenada no dispositivo. Nova senha… Remover Remover senha da Keystore\? Insira a senha atual correta. Atualizar senha da base de dados nova mensagem - Senha da base de dados & exportação - A base de dados está encriptada com uma senha aleatória. Por favor, altere-a antes de exportar. - A senha de encriptação da base de dados será atualizada. + Palavra-passe da base de dados & exportação + A base de dados está encriptada com uma palavra-passe aleatória. Por favor, altere-a antes de exportar. + A palavra-passe de encriptação da base de dados será atualizada. Por favor armazene a senha de forma segura, você NÃO será capaz de a alterar se a perder. - A senha de encriptação da base de dados será atualizada e armazenada na Keystore. + A palavra-passe de encriptação da base de dados será atualizada e armazenada nas definições. A base de dados será encriptada e a senha armazenada na Keystore. Por favor armazene a senha de forma segura, você NÃO será capaz de aceder às conversas se a perder. Para receber notificações, por favor, digite a senha da base de dados Hosts Onion não serão usados. Hosts Onion serão usados quando disponíveis. - Hosts Onion serão necessários para a conexão. chamada de vídeo (sem encriptação ponta a ponta) chamada de áudio (não encriptada ponta a ponta) chamada de áudio encriptada ponta a ponta @@ -624,18 +614,17 @@ oferecido %s Arquivo de base de dados antigo Hosts Onion serão necessários para a conexão. - Hosts Onion serão usados quando disponíveis. Pode acontecer quando você ou sua conexão usaram o backup de base de dados antigo. encriptado ponta a ponta desligado Ler código QR.]]> - contato não tem encriptação ponta a ponta + o contacto não tem encriptação ponta a ponta oferecido %s: %2s desligado ligado Agora os administradores podem: \n- eliminar mensagens de membros. -\n- desativar membros (função de \"observador\") +\n- desativar membros (função de "observador")
Ligação do grupo SimpleX Apenas 10 imagens podem ser enviadas ao mesmo tempo Apenas 10 vídeos podem ser enviados ao mesmo tempo @@ -645,9 +634,8 @@ Para verificar a encriptação de ponta a ponta com o seu contato, compare (ou leia) o código nos seus dispositivos. Ler o código de segurança a partir da aplicação do seu contacto. Ler o código QR do servidor - Hosts Onion não serão usados. - encriptação de ponta a ponta de 2 camadas.]]> - contato tem encriptação ponta a ponta + Apenas dispositivos pessoais armazenam perfis de utilizador, contatos, grupos e mensagens. + o contacto tem encriptação ponta a ponta sem encriptação ponta a ponta criador meses @@ -686,51 +674,51 @@ Email Isolamento do Transporte terminado - Ferramentas de desenvolvedor + Ferramentos de programador Encriptar Fazer downgrade e abrir chat - Activar TCP manter-vivo + Ativar TCP keep-alive predefinido (%s) Isolamento do Transporte A tentar connectar ao servidor usado para receber mensagens deste contacto. Erro de descodificação IDs das bases de dados e opções de isolamento do Transporte - Downgrade da base de dados - Activar SimpleX Lock + Regressão da base de dados + Ativar SimpleX Lock Editar editado Mensagem temporária Editar imagem - Activar bloqueio - Activar código de acesso auto-destrutivo - Activar auto-destruição + Ativar bloqueio + Ativar código de acesso auto-destrutivo + Ativar auto-destruição Um perfil vazio é criado com o nome fornecido, e a aplicação abre como de costume. - a versão da base de dados é mais recente do que a aplicação, mas sem migração para baixo para: %s - Desaparecerá: %s + a versão da base de dados é mais recente do que a aplicação, mas sem migração de regressão para: %s + Desaparecerá a: %s Eliminar ficheiros e multimédia\? Eliminado a: %s - A autenticação do dispositivo está desactivada. A desligar SimpleX Lock. - 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. + A autenticação do dispositivo está desativada. A desligar SimpleX Lock. + 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. migração diferente na aplicação/base de dados: %s / %s - Activar chamadas a partir do ecrã de bloqueio através das Definições. + Ativar chamadas a partir do ecrã de bloqueio através das Definições. Encriptar base de dados\? Introduzir o servidor manualmente - Activar a eliminação automática de mensagens\? + Ativar a eliminação automática de mensagens? Editar perfil de grupo Erro de desencriptação O código de acesso é substituído por um código auto-destrutivo. ID da base de dados: %d - Nomes, avatares e isolamento de transporte diferentes. + Nomes, fotos de perfil e isolamento de transporte diferentes. Secundário adicional Realce adicional Fundo - Customizar e partilhar temas de cor. + Personalizar e partilhar temas de cor. Temas personalizados Eliminado a - Desaparecerá - activado - activado para contacto - activado para si + Desaparecerá a + Ativo + ativo para contacto + ativo para si Pesquisar Desativado O teste falhou na etapa %s. @@ -772,7 +760,6 @@ %s, %s e %d membros Iniciar nova conversa Sistema - Parar conversa para habilitar ações do banco de dados Toque para participar %s, %s e %s conectado Tempo esgotado da conexão TCP @@ -780,4 +767,208 @@ Parar conversa Obrigado aos usuários – contribuam via Weblate! Função lenta - \ No newline at end of file + Entrega + As suas preferências + Desconectar dispositivos móveis + %d mensagens bloqueadas pelo administrador + Abrir porta na firewall + A sua privacidade + Ativar (manter sobreposição de grupo) + Abrir ecrã de migração + Erro de servidor do destino: %1$s + Você pode ativar o SimpleX Lock através das Definições. + Eliminar %d mensagens? + Criar perfil de chat + Abrir definições + Contactos + você saiu + você removeu %1$s + encriptação ok + Cancelar mudança de endereço + Você continuará a receber chamadas e notificações de perfis silenciados quando estes estão ativos. + Ativar em conversas diretas (BETA)! + Endereço de computador + Descobrível na rede local + Desktop tem uma versão não suportada. Por favor, confirme que usa a mesma versão em ambos os dispositivos + Desktop tem o código de convite errado + A transferir arquivo + A transferir detalhes de ligação + A criar ligação de arquivo + duplicados(as) + Abrir definições do servidor + O seu perfil de conversa será enviado +\npara o seu contacto + Conectar com %1$s? + Auricular + Dispositivos + Usar a partir de computador na aplicação mobile e ler código QR.]]> + Você enviou um convite de grupo + você mudou o seu cargo para %s + você mudou o endereço para %s + 6 novos idiomas de interface + A versão %s da aplicação de computador não é compatível com esta aplicação. + %s com a razão: %s]]> + O seu perfil %1$s vai ser partilhado. + Transferir + Cancelar + Opções de programador + Ativar + Computador + O seu contacto enviou um ficheiro que é maior que o tamanho máximo suportado atualmente (%1$s). + O seu servidor + O endereço do seu servidor + As tuas chamadas + Não enviar histórico a novos membros. + Você partilhou um caminho de ficheiro inválido. Relate o problema aos programadores da aplicação. + Você não tem conversas + Criar grupo + O seu perfil de conversa será enviado para os membros do grupo + Desktop foi disconectado + Transferência falhada + Desativar notificações + Abir SimpleX Chat para aceitar a chamada + Ativar (manter sobreposições) + Criado em + Ativo para + Os seus contactos podem permitir eliminação total de mensagens. + Desconectado com a razão: %s + Você já solicitou ligação através deste endereço! + Código e protocolo open-source - qualquer indivíduo pode ser anfitrião dos servidores. + %d mensagens bloqueadas + %d mensagens marcadas como eliminadas + Você não pode ser verificado; por favor tente novamente. + contacto eliminado + Cancelar mudança de endereço? + Você convidou este contacto + Corrigir nome para %s? + Descobrir na rede local + Ativar recibos para grupos? + Desativar para todos + Você desbloqueou %s + Você será conectado quando o pedido de ligação for aceite, por favor aguarde ou volte mais tarde! + O seu perfil atual + 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. + 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. + A base de dados será encriptada e a palavra-passe armazenada nas definições. + Você juntou-se a este group. A conectar ao membro que o convidou. + você mudou o cargo de %s para %s + contacto %1$s mudou para %2$s + você mudou de endereço + desativado + Criado em: %s + desativado + Descobrir e juntar a grupos + Dispositivos desktop + Compreendido + errors de desencriptação + Ligações + Criado + Eliminado + Erros de eliminação + Erro de cópia + Criar novo perfil na aplicação de computador. 💻 + Recibos de entrega! + Recibos de entrega desativados! + Não ativar + Você não pode enviar mensagens! + Eliminar e notificar contacto + Eliminar sem notificação + Conversas eliminadas + O seu endereço SimpleX + Os seus contactos + Os seus perfis de conversa + Você pode ver a ligação de convite outra vez nos detalhes da ligação. + A transferir atualização da app, não feche a aplicação + Transferir %s (%s) + Os seus servidores ICE + O seu perfil, contactos e mensagens entregues são armazenados no seu dispositivo. + Os seus servidores ICE + Desativar (manter as sobreposições de grupo) + Desativar (manter sobreposições) + Desativar recibos? + Desativar recibos para grupos? + Ativar para todos + Ativar para todos os grupos + Ativar recibos? + Você rejeitou o convite de grupo + Você juntou-se a este grupo + %d eventos de grupo + encriptação ok para %s + O pedido de ligação vai ser enviado para este membro do grupo. + O contacto foi eliminado. + Você precisa de permitir que o seu contacto inicie chamada de voz para poder iniciar uma chamada de voz. + Escuro + Cores de modo escuro + Modo escuro + Aproximar + Desktop está inativo + Perfil atual + Eliminar base de dados deste dispositivo + Estatísticas detalhadas + Transferido + Ficheiros transferidos + Erros de transferência + Conectar via link? + Criar perfil + A criar ligação… + Contacto eliminado! + Conversa eliminada! + Erro crítico + Nome deste dispositivo + O contacto será eliminado - esta ação é irreversível! + A palavra-passe de encriptação da base de dados será atualizada e armazenada nas definições. + Criar um grupo utilizando um perfil aleatório. + Migração da base de dados em progresso. +\nPode demorar alguns minutos. + Você será conectado quando o dispositivo do seu contacto estiver online, por favor aguarde ou volte mais tarde! + Desconectar + Desativado + Os seus servidores XFTP + Você controla a conversa! + Você poderá ver a conversa com %1$s na lista de conversas. + Os seus servidores SMP + Você pode usar markdown para formatar mensagens: + Você poderá enviar mensagens para %1$s através das conversas Eliminadas. + 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). + Os seus contactos permanecerão conectados. + Você terá que autenticar-se quando iniciar ou abrir a aplicação após 30 segundos em segundo plano. + Ativar acesso à câmara + Você precisa de permitir que o seu contacto envie mensagens de aúdio para poder enviá-las. + Desativar + Desativar para todos os grupos + A tua base de dados de conversas + Detalhes + Você pode partilhar o seu endereço com os seus contactos para permitir que se conectem com %s. + Você pode partilhar o seu endereço como uma ligação ou código QR - qualquer pessoa pode conectar-se a si. + Permitir + Todas as novas mensagens de %s serão ocultadas! + Somente você pode fazer ligações. + Todos os modos de cores + administradores + "aceitando criptografia para %s…" + Adicionar contato + Permitir downgrade + Todas as mensagens serão deletadas - isso não poderá ser desfeito! + Mais algumas coisas + Permitir envio de arquivos e mídias. + Administradores podem bloquear um membro para todos. + Aceitando criptografia + Configurações avançadas + todos os membros + Erros de reconhecimento + Mudança de endereço será cancelada. Antigo endereço de recebimento será usado. + Permitir ligações? + Conexões ativas + Todos os seus contatos, conversas e arquivos serão encriptados e enviados em chunks para relays XFTP configurados. + %1$d erro(s) de outro arquivo. + %1$s mensagens não encaminhadas + Sempre + Sempre use uma rota privata. + Permitir o envio de links SimpleX. + %1$d mensagens moderadas por %2$s + e %d outros + Todos os perfis + Já conectando! + Já entrando no grupo! + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index b6f268e6f1..92572516b9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -5,8 +5,8 @@ 30 secunde Acceptă Acceptă incognito - Adaugă server… - Setări de rețea avansate + Adaugă server + Reglări avansate de rețea %1$s dorește să se conecteze cu tine prin Acceptă %1$d mesaje omise. @@ -17,7 +17,7 @@ Adițional secundar Acceptă 6 limbi noi pentru interfață - %1$d mesaje nu au putut fi decriptate. + %1$d mesaje nu au putut fi descifrate. %1$d mesaj(e) omis(e) %1$s MEMBRI 1 zi @@ -39,8 +39,8 @@ Adaugă servere prin scanare de coduri QR. Adaugă la un alt dispozitiv Adaugă mesaj de întâmpinare - acceptând criptarea… - acceptând criptarea pentru %s… + se acceptă cifrarea… + se acceptă cifrarea pentru %s… Administratorii pot crea linkuri de participare la grupuri. Câteva lucruri în plus Toate datele aplicației sunt șterse. @@ -50,7 +50,7 @@ Întrerupe schimbarea adresei Întrerupe Toate conversațiile și mesajele vor fi șterse - operațiunea este definitivă! - Toate mesajele vor fi șterse - operațiunea este definitivă! + Toate mesajele vor fi șterse - acest lucru nu poate fi anulat! Permite reacții la mesaje doar dacă și contactul tău le permite. Permite ștergerea ireversibilă a mesajelor trimise. (24 ore) Permite trimiterea de fișiere și media. @@ -122,7 +122,7 @@ Înapoi Creează grup secret Se creează link… - apel audio (necriptat e2e) + apel audio (necifrat e2e) Creează fișier Adaugă contact: pentru a crea un nou link de invitare, sau a te conecta printr-un link pe care l-ai primit.]]> Autentificare anulată @@ -150,9 +150,659 @@ creator Fundal Creează profil nou în aplicația desktop. 💻 - Aplicația criptează fișierele locale noi (cu excepția videoclipurilor). + Aplicația cifrează fișierele locale noi (cu excepția videoclipurilor). autor Arabă, Bulgară, Finlandeză, Ebraică, Thailandeză și Ucraineană - mulțumită utilizatorilor și Weblate. Apelurile audio/video sunt interzise. - (prezent) - \ No newline at end of file + (actual) + Poză de profil eliminată + Temă întunecată + eliminat + Întunecată + Adresă de contact eliminată + Personalizează și distribuie teme colorate. + Teme personalizate + Repetă descărcarea + Timp personalizat + Elimină membru + Repetă importarea + Personalizează tema + Elimină + Elimină + Elimină membru + Elimini membrul? + Resetează la implicit + Resetează culoarea + Resetează culorile + Elimină imagine + Repetă încărcarea + apel respins + Elimini fraza de acces din Keystore? + Elimini parola din reglări? + Repetă cererea de conectare? + Necesar + Reîncearcă + Primește fișiere în siguranță + Grupuri mai sigure + Restabilește + Reîmprospătează + Revoci fișierul? + Revocă + Renegociezi cifrarea? + Resetează + Respinge + Salvează parola din reglări + Salvează și actualizează profilul grupului + Repetă cererea de alăturare? + Repornește conversația + salvat + Salvat de la %s + Salvează + Salvat + Salvat de la + Renegociază + Salvează servere + Servere WebRTC ICE salvate vor fi eliminate. + Salvează parola profilului + Salvează fraza de acces și deschide conversația + %s și %s + Renegociază cifrarea + Salvează și notifică contactul + Salvează și notifică contactele + Salvează și notifică membrii grupului + Rulează când aplicația este pornită + Răspunde + Revocă fișierul + Salvează + Salvezi reglările? + Salvezi preferințe? + Repornire + Restabilește copia de rezervă a bazei de date + Restabilești copia de rezervă a bazei de date? + Eroare la restabilirea bazei de date + %1$s eliminat + %s și %s conectați + Rol + Salvează + Arată + Respinge + Salvezi servere? + Apel respins + Mesaj salvat + Repornește aplicația pentru a crea un nou profil + Salvează fraza de acces în Keystore + Salvează profilul grupului + Repetă + Trimite previzualizări ale link-ului + Setează frază de acces + Distribuie adresă + Trimis la + Secundar + Mesaj trimis + Setează preferințele grupului + trimis + Adresa serverului este incompatibilă cu reglările rețelei. + Versiunea serverului este incompatibilă cu reglările rețelei. + Trimițând prin + trimiterea de fișiere nu este acceptată încă + Scanează codul de securitate din aplicația contactului tău + Selectează contacte + Autodistrugere + Răspuns trimis + Distribuie media… + Distribuie mesaj… + Arată lista conversațiilor într-o fereastră nouă + Arată consola într-o fereastră nouă + Setează fraza de acces a bazei de date + Setează fraza de acces a bazei de date + setează adresă de contact nouă + %s (actual) + Cod de sesiune + Expeditorul a anulat transferul de fișiere. + Serverul necesită autorizație pentru a crea cozi, verifică parola + Distribuie + trimitere eșuată + Caută sau lipește link SimpleX + Setează numele de contact + Salvezi mesajul de bun venit? + Evaluare de securitate + Scanează cod QR de pe desktop + Caută + Trimite un mesaj live - se va actualiza pentru destinatar(i) în timp ce îl tastezi + Distribuie fișier + Trimite până la ultimele 100 de mesaje membrilor noi. + Bara de căutare acceptă link-uri de invitație. + secunde + Scanează de pe mobil + Mesaj trimis + Scanează cod QR + Trimite + Trimite întrebări și idei + Arată opțiuni dezvoltator + sec + Mesajele trimise vor fi șterse după timpul setat. + Serverul necesită autorizație pentru a încărca, verifică parola + Arată contact și mesaje + Distribuie fișier… + Setează numele de contact… + Trimite mesaj + Trimite mesaj temporar + (scanează sau lipește din clipboard) + Arată cod QR + Test server eșuat! + Arată: + Arată erori interne + secret + REGLĂRI + %s conectat + setează imagine de profil + Trimis către: %s + SERVERE + Trimite mesaj live + %s descărcat + Distribui adresa cu contactele? + Arată previzualizare + trimite mesaj direct + Trimite mesaj direct pentru a te conecta + Selectează + Trimiterea de fișiere va fi oprită. + Trimite + Reglări + Scanează cod + Cod de securitate + Trimite-ne email + Scanează codul QR al serverului + Distribuie contactelor + Arată + cod de securitate schimbat + Arată ultimul mesaj + Trimite mesaj direct + Setează tema implicită + SimpleX + SimpleX nu poate rula în fundal. Vei primi notificările doar când aplicația rulează. + Serviciu SimpleX Chat + Adresă SimpleX + Închide + Link-uri SimpleX + Link-urile SimpleX sunt interzise în acest grup. + Securitatea SimpleX Chat a fost verificată de Trail of Bits. + Mesaje SimpleX Chat + Apeluri SimpleX Chat + Adresă SimpleX + simplexmq: v%s (%2s) + Link-uri SimpleX nepermise + Echipa SimpleX + Închizi? + Adresă de contact SimpleX + Link pentru grup SimpleX + Link-uri SimpleX + Invitație unică SimpleX + Siglă SimpleX + Grupuri mici (max 20) + Mod incognito simplificat + Pătrat, cerc, sau orice între. + %s nu este verificat + %s este verificat + Servere SMP + %s, %s și %d alți membri s-au conectat + %s, %s și %d membri + Difuzor + %s, %s și %s s-au conectat + %s: %s + Niște servere au eșuat testul: + Difuzor oprit + Difuzor pornit + Copie de rezervă a datelor aplicației + Tema aplicației + Accent suplimentar 2 + Începe conversația + Toate modurile de culoare + Folosește mereu releu + Aplică pentru + Începe o nouă conversație + Stea pe GitHub + Cifrare standard de la un capăt la altul + Pornește periodic + Mereu + Folosește mereu rutare privată. + Toate contactele vor rămâne conectate. Actualizarea profilului va fi trimisă contactelor tale. + pornire… + %s secunde + Începi conversația? + Reglări avansate + Adresă desktop rea + ID de mesaj incorect + Hash de mesaj incorect + ID de mesaj incorect + Hash de mesaj incorect + Schimbă adresa de primire + 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. + APELURI + ai schimbat rolul pentru tine la %s + Capacitate depășită - destinatarul nu a primit mesajele trimise anterior. + Schimbă codul de acces autodistructibil + Conversația este oprită + se schimbă adresa… + Contact verificat + Creat la + Migrează de pe alt dispozitiv pe dispozitivul nou și scanează codul QR.]]> + Te conectezi prin adresa de contact? + Te conectezi printr-un link unic? + Conectare incognito + Contactul deja există + Schimbă codul de acces + Poți porni Blocare SimpleX din Reglări. + Conversații + Alege un fișier + 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). + eroare apel + Apelurile tale + Schimbă + Schimbă rolul + Contactul permite + Grupuri mai bune + Desktop conectat + Preferințe contact + Te conectezi cu %1$s? + Anulează previzualizarea imaginii + Discută cu dezvoltatorii + Trebuie să introduci fraza de acces de fiecare dată când aplicația pornește - nu este stocată pe dispozitiv. + blocat + Preferințe conversație + Mod întunecat + Interfață chineză și spaniolă + Crează un grup folosind un profil aleatoriu. + Blochează membrii grupului + Mobil conectat + Conectat la desktop + Anulează migrarea + Celular + Nu poți trimite mesaje! + Schimbi adresa de primire? + Contactul nu este conectat încă! + Conectare prin link + Poți vedea linkul de invitație din nou în detaliile conexiunii. + Profilurile tale de conversație + Crează profil de conversație + apel terminat %1$s + Crează + Bluetooth + Camera + Apeluri pe ecranul blocat: + contactul are cifrare e2e + contactul nu are cifrare e2e + Contacte + DISCUȚII + BAZĂ DE DATE CONVERSAȚIE + Baza de date a conversației ștearsă + Conversația rulează + Baza ta de date a conversațiilor + Baza de date a conversațiilor tale nu este cifrată - pune o parolă pentru a o proteja. + Parola de cifrare a bazei de date va fi actualizată și stocată în reglări. + ai schimbat rolul %s la %s + Nu se pot invita contactele! + Te-ai alăturat grupului + Nu se poate invita contactul! + se conectează (acceptat) + Creat la: %s + Blochează + Blochează membru + Conectare directă? + Profilul tău de conversație va fi trimis membrilor grupului + Întunecat + Culori mod întunecat + Și tu și contactul tău puteți face apeluri. + %s anulat + Conectat la mobil + Eroare copiere + Te conectezi prin link? + Nu ai putut fi verificat(ă); te rog încearcă din nou. + Copiază + Anulează mesajul live + Conectare prin link / cod QR + Contactele tale vor rămâne conectate. + Parola de cifrare a bazei de date va fi actualizată. + Contactele tale pot permite ștergerea totală a mesajelor. + Trebuie să permiți contactului tău să trimită mesaje vocale pentru a le putea trimite. + Versiunede bază: v%s + Creează o adresă pentru a permite oamenilor să se conecteze cu tine. + %s blocat + ai schimbat adresa + ai schimbat adresa pentru %s + se schimbă adresa… + se schimbă adresa pentru %s… + Și tu și contactul tău puteți trimite mesaje temporare. + Nu se pot primi fișiere + Poate fi dezactivat din reglări – notificările vor fi afișate dacă aplicația este în funcțiune.]]> + Verifică mesajele noi la fiecare 10 minute timp de până la 1 minut + Nu ai conversații + Contactul și toate mesajele vor fi șterse - acest lucru nu poate fi anulat! + Copiat în clipboard + Camera + Ai invitat un contact + Profilul tău de conversație va fi trimis +\ncontactului tău + Contribuie + Profil conversație + aldin + Poți folosi markdown pentru a formata mesaje: + se apelează… + Schimbă modul de autodistrugere + Baza de date a conversației importată + 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. + Nu se poate accesa Keystore pentru a salva parola bazei de date + Conversație migrată! + Continuă + apel în curs + Apel în curs + Și tu și contactul tău puteți trimite mesaje vocale. + Contact ascuns: + Nume contact + Crează adresă + Bază de date cifrată! + Schimbi fraza de acces a bazei de date? + Conversația este oprită + Poți porni discuția din Reglările aplicației / Baza de date sau repornind aplicația. + ai ieșit + Blochezi membrul? + Blocat de admin + Și tu și contactul tău puteți adăuga reacții la mesaje. + Mesaje mai bune + blocat + blocat de admin + Nu se poate inițializa baza de date + Contactul tău a trimis un fișier care este mai mare decât dimensiunea maximă suportată în prezent (%1$s). + anulează previzualizarea link-ului + Verifică adresa serverului și încearcă din nou. + Tu îți controlezi conversația! + Apel deja terminat! + Apel terminat + Blochează pentru toți + Ai cerut deja conexiunea prin această adresă! + Te conectezi la tine? + Verifică conexiunea la internet și încearcă din nou + Culori conversație + Aspectul discuției + Bun pentru baterie. Aplicația verifică mesajele la fiecare 10 minute. Pot fi pierdute apeluri sau mesaje urgente.]]> + Negru + Blochezi membrul pentru toți? + Și tu și contactul tău puteți șterge ireversibil mesajele trimise. (24 de ore) + Folosește mai multă baterie! +\nServiciul în fundal rulează mereu – notificările sunt afișate imediat ce mesajele sunt disponibile. + Nu se poate trimite mesajul + Ștergeți + Confirmați fișiere de la servere necunoscute. + schimbat adresa pentru dumneavoastră + Confirmați parola nouă… + Conectare + conexiune %1$d + conexiune stabilită + Eroare de conexiune + Conexiune expirată + se conectează + Confirmare + În curând! + se conectează… + Schimbați modul de blocare + Versiunea aplicației: %s + pentru fiecare profil de conversație pe care le aveți în aplicație]]> + Permiteți downgrade-ul + pentru fiecare contact și membru de grup \nVa rugăm considerați că: dacă aveți prea multe conexiuni, consumul dumneavoastră de baterie și trafic de internet pot fi considerabil mai mari, iar unele conexiuni pot eșua.]]> + Conexiune terminată + Se conectează la desktop + Rugat să primească imaginea + Cerere de conexiune trimisă! + De reținut: releele de mesaje și fișiere sunt conectate prin proxy SOCKS. Apelurile și trimiterea de previzualizări ale adreselor web utilizează conexiunea directă.]]> + conectat + Confirmați codul de access + Confirmare actualizare bază de date + "schimbat rolul lui %s la %s" + conectat + se conectează (introdus) + conectat + se conectează + Schimbați rolul de grup? + Modul color + Prin profil de conversație (implicit) sau prin conexiune (BETA). + Comparați codurile de securitate cu contactele dumneavoastră. + Conexiune oprită + Conexiune oprită + Conectare la desktop + Conexiunea la desktop este într-o stare proastă + Conectare + Conexiune + Ștergi notițele private? + Conectare automată + se conectează apelul… + se conectează (invetație la introducere) + se conectează… + %s este într-o stare proastă]]> + Confirmă reglările rețelei + se conectează… + Eroare de conexiune (AUTENTIFICARE) + 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. + conectat + Crează un grup: pentru a crea un nou grup.]]> + Ștergeți conversația? + Ștergeți + Ștergeți conversația + Conectare + Consolă conversație + Confirmați parola + colorat + 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. + Rugat să primească videoclipul + Comparați fișierul + Vă rugăm să rețineți: nu veți putea recupera sau schimba parola dacă o veți pierde.]]> + schimbat rolul dumneavoastră la %s + conectat + se conectează + conectat + Ștergeți + Buton de închidere + Ștergeți verificarea + Configurare servere ICE + conectat direct + se conectează (anunțat) + Cel mai bun pentru baterie. Veți primi notificări doar când aplicația rulează (FĂRĂ servicii de fundal).]]> + Se conectează apelul + complet + De reținut: folosirea aceleiași baze de date în două aparate, va intrerupe descifrarea mesajelor din conexiunile tale, ca măsură de protecție.]]> + Confirmați încărcarea + Confirmați că țineți minte parola de la baza de date pentru a o migra. + Conexiune + Ștergi profilul de conversație? + Șterge pentru mine + %dd + șters + Șterge contact + Șters la + Versiunea aplicației desktop %s nu este compatibilă cu această aplicație. + Ștergi mesajul membrului? + Șterge grup + Șterge și notifică contactele + Decentralizat + grup șters + %d contact(e) selectat(e) + Șters la: %s + Ștergi profilul de conversație? + Șterge profil + implicit (%s) + Confirmări de livrare! + Confirmările de livrare sunt dezactivate! + Adresă desktop + Dispozitive desktop + Desktop + Eroare de decriptare + Șterge imagine + Șterge după + Șterge pentru toată lumea + Descriere + Șterge fișier + Șterge coadă + Ștergi contactul? + Șterge + Șterge + Ștergi fișiere și media? + %d zile + %d zi + Șterge adresa + Șterge mesaje + Ștergi %d mesaje? + Ștergi adresa? + Șterge toate fișierele + Șterge link + Ștergi link? + zile + Șterge + Ștergi mesajul? + Livrare + Ștergi conexiunea în așteptare? + Șterge server + Șterge baza de date + contact șters + Ștergi grupul? + Șterge baza de date de pe acest dispozitiv + Șterge profil de conversație + Șterge fișiere pentru toate profilurile de conversație + Oricine poate găzdui servere. + Contacte arhivate + Beta + Apeluri interzise! + Nu se poate apela membrul grupului + Bază de date conversație exportată + Verifică pentru actualizări + Contactul %1$s a schimbat la %2$s + Contactul este șters. + Controlează-ți rețeaua + Parola pentru cifrarea bazei de date va fi actualizată și stocată în Keystore. + Fraza de acces a bazei de date + Servere XFTP configurate + Baza de date este cifrată folosind o parolă aleatorie. Trebuie schimbată înainte de exportare. + apel + conectare + Contact șters! + Conversație ștearsă! + Actualizarea aplicației este descărcată + Verifică pentru actualizări + Creează + Estompează media + BAZĂ DE DATE DISCUȚIE + Conectează-te cu prietenii mai ușor. + încercări + Completat + ID-urile bazei de date și opțiunea de izolare a transportului. + ID bază de date + ID bază de date: %d + Conexiuni + Creat + Estompează pentru intimitate mai bună. + Stare conexiune și servere + Eroare bază de date + Contactele pot marca mesajele pentru ștergere; tu le vei putea vedea. + Downgrade al bazei de date + Migrarea bazei de date este în proces. +\nPoate dura câteva minute. + Fraza de acces a bazei de date și export + Toate profilurile + Profil actual + Confirmi ștergerea contactului? + Contactul va fi șters - acest lucru nu poate fi anulat! + Servere SMP configurate + Conectat + Corectează numele la %s? + Continuă + Baza de date este cifrată folosind o parolă aleatorie; o poți schimba. + Nu se pot trimite mesaje membrului grupului + Se conectează + Servere conectate + Nu se poate apela contactul + Se conectează la contact, așteaptă sau verifică mai târziu! + Reglări avansate + Reglări + Reglările tale + apel vocal cifrat e2e + apel video cifrat e2e + APARAT + EXPERIMENTAL + Cifrează + erori la descifrare + TU + necifrat e2e + cifrat e2e + Apel video primit + Dezactivează + Permiți apeluri? + terminat + Sunete la apel + Apeluri îmbunătățite + Apel audio primit + Termină apelul + marcate şterse + %d mesaje marcate șterse + moderat de %s + %1$d alte erori de fișier. + %1$s mesaje netransmise + ai distribuit un link de unică folosință + ai distribuit un link ascuns de unică folosință + prin link pentru grup + ascuns prin link pentru grup + Prin browser + Reclamă + Conținut inadecvat + Încălcă normelor comunitare + Profil inadecvat + Alt motiv + Eroare la salvarea serverelor SMP + Asigurați-vă că adresele serverelor XFTP sunt în format corect, pe randuri separate și nu sunt duplicate. + Eroare la încărcarea serverlor XFTP + Eroare la actualizarea configurației de rețea + Eșec la încărcarea conversațiilor + Vă rugăm să actualizați aplicația și contactați dezvoltatorii. + Eroare la crearea profilului! + %d mesaje blocate + Eroare de renegociere a criptării + invitat să se conecteze + ascuns prin link pentru adresa de contact + printr-un link de unică folosință + 1 raport + 1 an + înaintat + eroare de afișare a mesajului + eroare de afișat conținutul + criptare cap-coadă cu secretizare înaintată perfecta, repudiere si recuperare în caz de spargere.]]> + criptare cap-coadă rezistentă la algoritmi cuantici cu secretizare înaintată perfecta, repudiere si recuperare în caz de spargere.]]> + Această conversație este protejată prin criptare cap-coadă. + Link întreg + ascuns printr-un link de unică folosință + a + b + prin %1$s + Eroare la încărcarea serverlor SMP + %1$d fișier(e) șterse. + raport arhivat de %s + invitație acceptată + Asigurați-vă că adresele serverelor SMP sunt în format corect, pe randuri separate și nu sunt duplicate. + Eroare la salvarea serverelor XFTP + %1$d fișier(e) încă în descărcare. + %1$d fișier(e) a eșuat să se descărcarce. + %1$d fișier(e) nu au fost descărcate. + prin link pentru adresa de contact + Deschiderea link-ului în browser poate reduce confidențialitatea și securitatea conexiunii. Link-urile SimpleX de neîncredere vor fi roșii. + solicitat să se conecteze + Eșec la încărcarea conversației + Doar tu și moderatorii vedeți asta + Doar expeditorul şi moderatorii văd asta + raport arhivat + %d mesaje blocate de admin + primirea de fișiere nu este acceptată încă + tu + format mesaj necunoscut + format mesaj invalid + LIVE + moderat + conversație nevalidă + date nevalide + Această conversație este protejată prin criptare cap-coadă, rezistentă la algoritmi cuantici. + Notițe private + 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 a71638eae0..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 @@ Мгновенные уведомления Мгновенные уведомления! Мгновенные уведомления выключены! - фоновый сервис SimpleX, который потребляет несколько процентов батареи в день.]]> + SimpleX выполняется в фоне вместо уведомлений через сервер.]]> Он может быть выключен через Настройки – Вы продолжите получать уведомления о сообщениях пока приложение запущено.]]> - разрешите SimpleX выполняться в фоне в следующем диалоге. Иначе уведомления будут выключены.]]> + Разрешите это в следующем окне чтобы получать нотификации мгновенно.]]> Оптимизация батареи включена, поэтому сервис уведомлений выключен. Вы можете снова включить его через Настройки. Периодические уведомления Периодические уведомления выключены! @@ -354,7 +354,7 @@ SMP серверы Адрес сервера по умолчанию Добавить серверы по умолчанию - Добавить сервер… + Добавить сервер Тестировать сервер Тестировать серверы Сохранить серверы @@ -394,7 +394,6 @@ Соединяться с серверами через SOCKS прокси через порт %d? Прокси должен быть запущен до включения этой опции. Использовать прямое соединение с Интернет? Если Вы подтвердите, серверы смогут видеть Ваш IP адрес, а провайдер - с какими серверами Вы соединяетесь. - Обновить настройки .onion хостов? Использовать .onion хосты Когда возможно Нет @@ -403,9 +402,6 @@ Onion хосты не используются. Подключаться только к onion хостам. \nОбратите внимание: Вы не сможете соединиться с серверами, у которых нет .onion адреса. - Onion хосты используются, если возможно. - Onion хосты не используются. - Подключаться только к onion хостам. Интерфейс Создать адрес @@ -449,7 +445,7 @@ Эта строка не является ссылкой-приглашением! Открыть в приложении.]]> - входящий звонок… + звонок… пропущенный звонок отклоненный звонок принятый звонок @@ -467,23 +463,20 @@ соединено завершен - Новое поколение приватных сообщений + Будущее коммуникаций Более конфиденциальный - Первая в мире платформа без идентификаторов пользователей. + Без идентификаторов пользователей. Защищен от спама - С Вами можно соединиться только через созданные Вами ссылки. + Вы определяете, кто может соединиться. Децентрализованный - Открытый протокол и код - кто угодно может запустить сервер. + Кто угодно может запустить сервер. Создать профиль Добавьте контакт Как это работает Как SimpleX работает - как SimpleX доставляет сообщения без идентификаторов пользователей?]]> - Чтобы защитить Вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, SimpleX использует ID для очередей сообщений, разные для каждого контакта. - получаете сообщения, Ваши контакты - серверы, которые Вы используете для отправки.]]> - с двухуровневым end-to-end шифрованием.]]> - Узнайте больше из нашего GitHub репозитория. + Чтобы защитить Вашу конфиденциальность, SimpleX использует разные ID для всех ваших контактов. + Только пользовательские устройства хранят контакты, группы и сообщения. GitHub репозитория.]]> Использовать чат @@ -607,7 +600,6 @@ Данные чата удалены Перезапустите приложение, чтобы создать новый профиль. Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от некоторых контактов. - Остановите чат, чтобы разблокировать операции с архивом чата. Удалить файлы во всех профилях чата Удалить все файлы Удалить файлы и медиа? @@ -680,12 +672,6 @@ Чат остановлен Вы можете запустить чат через Настройки приложения или перезапустив приложение. - Архив чата - АРХИВ ЧАТА - Сохранить архив - Удалить архив - Дата создания %1$s - Удалить архив чата? приглашение в группу %1$s Вступить в группу? @@ -768,7 +754,7 @@ Вы пытаетесь пригласить инкогнито контакт в группу, где Вы используете свой основной профиль Пригласить членов группы - УЧАСТНИКОВ ГРУППЫ: %1$s + %1$s ЧЛЕНОВ ГРУППЫ Вы: %1$s Удалить группу Удалить группу? @@ -800,8 +786,8 @@ Поменять Переключить Поменять роль в группе? - Роль будет изменена на \"%s\". Все в группе получат сообщение. - Роль будет изменена на \"%s\". Будет отправлено новое приглашение. + Роль будет изменена на "%s". Все в группе получат сообщение. + Роль будет изменена на "%s". Будет отправлено новое приглашение. Ошибка при удалении члена группы Ошибка при изменении роли Группа @@ -827,11 +813,10 @@ Сбросить настройки сек - Таймаут TCP соединения + Таймаут TCP-соединения Таймаут протокола Интервал PING Включить TCP keep-alive - Отменить изменения Сохранить Обновить настройки сети? Обновление настроек приведет к переподключению клиента ко всем серверам. @@ -848,7 +833,6 @@ Темная Тема - Сохранить цвет Сбросить цвета Акцент @@ -893,20 +877,20 @@ Запретить необратимое удаление сообщений. Разрешить отправлять голосовые сообщения. Запретить отправлять голосовые сообщений. - Члены группы могут посылать прямые сообщения. - Прямые сообщения между членами группы запрещены. - Члены группы могут необратимо удалять отправленные сообщения. (24 часа) - Необратимое удаление сообщений запрещено в этой группе. - Члены группы могут отправлять голосовые сообщения. - Голосовые сообщения запрещены в этой группе. + Члены могут посылать прямые сообщения. + Прямые сообщения между членами группы запрещены. + Члены могут необратимо удалять отправленные сообщения. (24 часа) + Необратимое удаление сообщений запрещено. + Члены могут отправлять голосовые сообщения. + Голосовые сообщения запрещены. Минимальный расход батареи. Вы получите уведомления только когда приложение запущено, без фонового сервиса.]]> Уведомления Когда приложение запущено Периодически Мгновенно - Больше расход батареи! Фоновый сервис постоянно запущен - уведомления будут показаны как только есть сообщения.]]> - Меньше расход батареи. Фоновый сервис проверяет сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.]]> - Можно изменить позже в настройках. + Больше расход батареи! Приложение постоянно запущено в фоне - уведомления будут показаны сразу же.]]> + Меньше расход батареи. Приложение проверяет сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.]]> + Как это влияет на потребление энергии LIVE Отправить живое сообщение Живое сообщение! @@ -942,7 +926,7 @@ Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите Создать ссылку группы Запретить отправлять исчезающие сообщения. - Исчезающие сообщения запрещены в этой группе. + Исчезающие сообщения запрещены. %dнед %dд %d нед. @@ -955,7 +939,7 @@ Сбросить подтверждение Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам. Запретить посылать исчезающие сообщения. - Члены группы могут посылать исчезающие сообщения. + Члены могут посылать исчезающие сообщения. Что нового Новое в %s Аудит безопасности @@ -970,7 +954,7 @@ Исчезающие сообщения Отправленные сообщения будут удалены через заданное время. Улучшенная конфигурация серверов - \"Живые\" сообщения + "Живые" сообщения Получатели видят их в то время как Вы их набираете. Проверить безопасность соединения Сравните код безопасности с Вашими контактами. @@ -1002,8 +986,7 @@ Все чаты и сообщения будут удалены - это нельзя отменить! Сборка приложения: %s Версия приложения: v%s - Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться для каждого контакта и члена группы. -\nОбратите внимание: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать. + для каждого контакта и члена группы. \nОбратите внимание: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.]]> для каждого профиля чата, который Вы имеете в приложении.]]> Версия ядра: v%s Удалить профиль чата\? @@ -1071,7 +1054,7 @@ Дополнительные улучшения скоро! Теперь админы могут: \n- удалять сообщения членов. -\n- приостанавливать членов (роль \"наблюдатель\") +\n- приостанавливать членов (роль "наблюдатель")
Защитите Ваши профили чата паролем! Раскрыть Поддержка bluetooth и другие улучшения. @@ -1149,7 +1132,7 @@ Ошибка расшифровки Блокировка SimpleX не включена! Ошибка хэш сообщения - Хэш предыдущего сообщения отличается. + Хэш предыдущего сообщения отличается\" Подтвердить код Неправильный код Заблокировать через @@ -1209,7 +1192,7 @@ Установить код доступа История Информация - Открыть профили чата + Изменить профили чата Полученное сообщение Отправленное сообщение Исчезающее сообщение @@ -1243,14 +1226,14 @@ Разрешить реакции на сообщения. Разрешить реакции на сообщения, только если ваш контакт разрешает их. Разрешить контактам добавлять реакции на сообщения. - Члены группы могут добавлять реакции на сообщения. + Члены могут добавлять реакции на сообщения. Реакции на сообщения в этом чате запрещены. - Реакции на сообщения запрещены в этой группе. + Реакции на сообщения запрещены. Только Ваш контакт может добавлять реакции на сообщения. Запретить реакции на сообщения. Запретить реакции на сообщения. секунд - ЦВЕТА ТЕМЫ + ЦВЕТА ИНТЕРФЕЙСА Поделиться адресом с контактами\? Обновлённый профиль будет отправлен Вашим контактам. Об адресе SimpleX @@ -1328,7 +1311,7 @@ Давайте поговорим в SimpleX Chat Открытие базы данных… Запись обновлена - Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли. + Во время импорта произошли некоторые ошибки: нет текста Поиск Отключено @@ -1364,8 +1347,8 @@ Нотификации перестанут работать, пока вы не перезапустите приложение Таймаут протокола на KB Разрешить посылать файлы и медиа. - Члены группы могут слать файлы и медиа. - Файлы и медиа запрещены в этой группе. + Члены могут слать файлы и медиа. + Файлы и медиа запрещены. Файлы и медиа запрещены! Только владельцы группы могут разрешить файлы и медиа. Файлы и медиа @@ -1693,6 +1676,7 @@ Внутренняя ошибка Очистить личные заметки? Новый чат + Новое сообщение Или отсканируйте QR код Вы можете увидеть ссылку-приглашение снова открыв соединение. Показывать медленные вызовы API @@ -1833,7 +1817,7 @@ Ссылки SimpleX Разрешить отправлять ссылки SimpleX. Запретить отправку ссылок SimpleX - Члены группы могут отправлять ссылки SimpleX + Члены могут отправлять ссылки SimpleX админы все члены владельцы @@ -1843,7 +1827,7 @@ Включено для Переслать Переслать и сохранить сообщение - Ссылки SimpleX запрещены в этой группе. + Ссылки SimpleX запрещены. Переслать сообщение… Литовский интерфейс Источник сообщения остаётся конфиденциальным. @@ -1851,4 +1835,636 @@ Форма картинок профилей Квадрат, круг и все, что между ними. Будет включено в прямых разговорах! - \ No newline at end of file + ФАЙЛЫ + Новые темы чатов + нет + Светлая + Системная + Цвета тёмного режима + Получайте файлы безопасно + Конфиденциальная доставка 🚀 + Улучшенная доставка сообщений + Уменьшенный расход батареи. + Версия сервера несовместима с настройками сети. + Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено. + Превышено количество сообщений - предыдущие сообщения не доставлены. + Ошибка сервера получателя: %1$s + Ошибка: %1$s + Пересылающий сервер: %1$s +\nОшибка сервера получателя: %2$s + Пересылающий сервер: %1$s +\nОшибка: %2$s + Предупреждение доставки сообщения + Ошибка сети - сообщение не было отправлено после многократных попыток. + Адрес сервера несовместим с настройками сети. + информация сервера об очереди: %1$s +\n +\nпоследнее полученное сообщение: %2$s + Показать список чатов в новом окне + Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов или когда SOCKS-прокси включен). + Незащищённый + Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов. + Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + Черная + Тёмный режим + Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку. + Режим цветов + Разрешить прямую доставку + Всегда + Подтверждать файлы с неизвестных серверов. + Всегда использовать конфиденциальную доставку. + Тёмная + Отладка доставки + Ошибка инициализации WebView. Обновите Вашу систему до новой версии. Свяжитесь с разработчиками. +\nОшибка: %s + Светлый режим + Сделайте ваши чаты разными! + Информация об очереди сообщений + Персидский интерфейс + Защитить IP адрес + Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. \nВключите в настройках Сети и серверов. + Отправьте сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + Конфиденциальная доставка + Использовать конфиденциальную доставку с неизвестными серверами. + Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен. + Когда IP защищен + Да + Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений. + Изображения профилей + Все режимы + Тема приложения + Сбросить на тему приложения + Сбросить на тему пользователя + Неизвестные серверы! + Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: +\n%1$s. + Не использовать конфиденциальную маршрутизацию. + Никогда + Неизвестные серверы + Нет + Показать статус сообщения + Прямая доставка сообщений + Режим доставки сообщений + КОНФИДЕНЦИАЛЬНАЯ ДОСТАВКА СООБЩЕНИЙ + Цвета чата + Тема чата + Тема профиля + Дополнительный акцент 2 + Дополнительные настройки + Обрезать + Полностью + Добрый день! + Доброе утро! + Полученный ответ + Удалить изображение + Повторить + Сбросить цвет + Масштаб + Отправленный ответ + Установить тему по умолчанию + Рисунок обоев + Фон обоев + Применить к + Не удается отправить сообщение + Бета + Соединeно + попытки + Готово + Потвердить удаление контакта? + Размытие изображений + Все профили + Проверка на наличие обновлений + Разрешить звонки? + Звонки запрещены! + Не удается позвонить члену группы + Обновление скачано + звонок + Не удается позвонить контакту + Не удается написать члену группы + Проверять обновления + соединиться + Адрес сервера назначения %1$s несовместим с настройками пересылающего сервера %2$s. + Ошибка подключения к пересылающему серверу %1$s. Попробуйте позже. + Пересылающий сервер %1$s не смог подключиться к серверу назначения %2$s. Попробуйте позже. + Версия пересылающего сервера несовместима с настройками сети: %1$s. + Версия сервера назначения %1$s несовместима с пересылающим сервером %2$s. + Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален. + Выбранные настройки чата запрещают это сообщение. + Ошибка файла + Сканировать / Вставить ссылку + Другие XFTP серверы + Настроенные XFTP серверы + Загрузка %s (%s) + Доступно обновление: %s + Выключить + Статус сообщения: %s + Размер шрифта + Пожалуйста, проверьте, что мобильный и компьютер находятся в одной и той же локальной сети, и что брандмауэр компьютера разрешает подключение. +\nПожалуйста, поделитесь любыми другими ошибками с разработчиками. + Файлы + Соединяется + Подробности + Всего + Активные соединения + Прием сообщений + В ожидании + Загружено + Статистика серверов будет сброшена - это нельзя отменить! + Всего отправлено + Переподключить + SMP сервер + Начиная с %s. + XFTP сервер + дубликаты + истекло + другое + Соединения + Ошибки удаления + Создано + Удалено + Защищено + Подписано + Блоков загружено + Ошибки загрузки + Размер + Ошибки приема + Принятые файлы + Адрес сервера + Ошибка сервера файлов: %1$s + Статус файла + Временная ошибка файла + Сильное + Подтверждено + Ошибки подтверждения + ошибки расшифровки + другие ошибки + Проксировано + Ошибки отправки + Блоков удалено + Блоков принято + Подписок игнорировано + Ошибка копирования + видеозвонок + Контакт будет удален — это нельзя отменить! + Оставить разговор + Удалить только разговор + Удалить без уведомления + Вы можете отправлять сообщения %1$s из Архивированных контактов. + Вставить ссылку + Нет отфильтрованных контактов + Ваши контакты + Архивированные контакты + Настроенные SMP серверы + Показать процент + Слабое + Среднее + Выключено + Доступная панель приложения + Текущий профиль + Нет информации, попробуйте перезагрузить + Информация о серверах + Информация по + Подключенные серверы + Ранее подключенные серверы + Проксированные серверы + Начиная с %s.\nВсе данные хранятся только на вашем устройстве. + Переподключить сервер для устранения неполадок доставки сообщений. Это использует дополнительный трафик. + Ошибка + Ошибка переподключения к серверу + Ошибка переподключения к серверам + Сбросить + Подробная статистика + Принято + Ошибка сброса статистики + Сбросить всю статистику? + Пожалуйста, попробуйте позже. + Ошибка конфиденциальной доставки + Адрес сервера несовместим с сетевыми настройками: %1$s. + Версия сервера несовместима с вашим приложением: %1$s. + Ошибки подписки + Отправленные файлы + Удалить %d сообщений членов группы? + Сообщения будут помечены на удаление. Получатель(и) смогут посмотреть эти сообщения. + Выбрать + Сообщения будут удалены для всех членов группы. + Сообщения будут помечены как удаленные для всех членов группы. + Контакт удален! + Разговор удален! + Член неактивен + Прямого соединения пока нет, сообщение переслано или будет переслано админом. + Ничего не выбрано + открыть + поиск + Выбрано %d + Настройки + Вы по-прежнему можете просмотреть разговор с %1$s в списке чатов. + Другие SMP серверы + Выключено + Установлено успешно + Установить обновление + Открыть расположение файла + Пожалуйста, перезапустите приложение. + Напомнить позже + Стабильная версия + Загрузка обновления отменена + Статус файла: %s + Данные чата экспортированы + Продолжить + Серверы файлов и медиа + Серверы сообщений + SOCKS прокси + Некоторые файл(ы) не были экспортированы + Вы можете мигрировать экспортированную базу данных. + Вы можете сохранить экспортированный архив. + выключен + неактивен + Пригласить + Статус сообщения + Контакт соединяется, подождите или проверьте позже! + Контакт удален. + Попросите Вашего контакта разрешить звонки. + Сохранить и переподключиться + Отправьте сообщение, чтобы включить звонки. + TCP-соединение + Чтобы включить звонки, разрешите их Вашему контакту. + Масштабирование + Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере. + Ошибки + Получено сообщений + Сообщений отправлено + Переподключить все подключенные серверы для устранения неполадок доставки сообщений. Это использует дополнительный трафик. + Переподключить все серверы + Переподключить сервер? + Переподключить серверы? + Сбросить всю статистику + Статистика + Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка. + Соединяйтесь с друзьями быстрее + Управляйте своей сетью + Защищает ваш IP адрес и соединения. + Открыть настройки серверов + Полученные сообщения + Ошибки приема + Архивируйте контакты чтобы продолжить переписку. + Отправлено напрямую + Отправлено через прокси + Транспортные сессии + Состояние соединения и серверов. + Удаляйте до 20 сообщений за раз. + Загрузка обновления, не закрывайте приложение. + Файл не найден - скорее всего, файл был удален или отменен. + Адрес пересылающего сервера несовместим с настройками сети: %1$s. + написать + Сообщение + Сообщение может быть доставлено позже, если член группы станет активным. + Сообщение переслано + Доступная панель чата + Всего получено + Пропустить эту версию + Чтобы получать уведомления об обновлениях, включите периодическую проверку стабильных или бета-версий. + Используйте приложение одной рукой. + Отправленные сообщения + Размыть для конфиденциальности. + Создать + Новые медиа-опции + Пригласить + Новое сообщение + Переключите список чатов: + Обновление приложения + Загружать новые версии из GitHub. + Увеличить размер шрифтов. + Новый интерфейс 🎉 + Открыть из списка чатов. + Сбросить все подсказки. + Вы можете изменить это в настройках Интерфейса. + Пересылка %1$s сообщений + Сохранение %1$s сообщений + Убедитесь, что конфигурация прокси правильная. + Аутентификация прокси + Использовать случайные учетные данные + Режим системы + Ошибка пересылки сообщений + %1$d ошибок файлов:\n%2$s + %1$d других ошибок файлов. + Переслать %1$s сообщение(й)? + Переслать сообщения без файлов? + Сообщения были удалены после того, как вы их выбрали. + Нет сообщений, которые можно переслать! + %1$d файл(ов) загружаются. + %1$d файл(ов) не удалось загрузить. + %1$d файлов было удалено. + %1$d файлов не было загружено. + Загрузить + %1$s сообщений не переслано + Переслать сообщения… + Проверьте правильность ссылки SimpleX. + Неверная ссылка + БАЗА ДАННЫХ + Ошибка инициализации WebView. Убедитесь, что у вас установлен WebView и его поддерживаемая архитектура – arm64.\nОшибка: %s + Звук отключен + Сообщения будут удалены — это нельзя отменить! + Ошибка переключения профиля + Выберите профиль чата + Поделиться профилем + Соединение было перемещено на %s, но при смене профиля произошла неожиданная ошибка. + Угол + Сессия приложения + Новые учетные данные SOCKS будут использоваться при каждом запуске приложения. + Новые учетные данные SOCKS будут использоваться для каждого сервера. + Сервер + Форма сообщений + Хвост + Нажмите кнопку информации рядом с адресной строкой, чтобы разрешить микрофон. + Откройте Настройки Safari / Веб-сайты / Микрофон, затем выберите Разрешить для localhost. + Улучшенные звонки + Улучшенные даты сообщений. + Улучшенная безопасность ✅ + Улучшенный интерфейс + Настраиваемая форма сообщений. + Удаляйте или модерируйте до 200 сообщений. + Пересылайте до 20 сообщений за раз. + Переключайте звук и видео во время звонка. + Переключайте профиль чата для одноразовых приглашений. + Аудит SimpleX протоколов от Trail of Bits. + Чтобы совершать звонки, разрешите использовать микрофон. Завершите вызов и попробуйте позвонить снова. + Не использовать учетные данные с прокси. + Ошибка сохранения прокси + Пароль + Использовать разные учетные данные прокси для каждого соединения. + Использовать разные учетные данные прокси для каждого профиля. + Имя пользователя + Ваши учетные данные могут быть отправлены в незашифрованном виде. + Удалить архив? + Загруженный архив базы данных будет навсегда удален с серверов. + Принятые условия + Принять условия + Нет серверов сообщений. + Нет серверов для приема сообщений. + Ошибки в настройках серверов. + Для профиля %s: + Нет серверов файлов и медиа. + Нет серверов для приема файлов. + Нет серверов для отправки файлов. + Недоставленные сообщения + Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. + Адрес или одноразовая ссылка? + Безопасность соединения + Операторы серверов + Ваши серверы + Посмотреть условия + Посмотреть условия + %s.]]> + Условия будут автоматически приняты для включенных операторов: %s + Условия приняты: %s. + Вебсайт + %s.]]> + %s.]]> + %s, примите условия использования.]]> + Для оправки + Дополнительные серверы сообщений + Использовать для файлов + Открыть условия + Ошибка добавления сервера + Сервер оператора + Сервер добавлен к оператору %s. + Тулбары приложения + Прозрачность + Децентрализация сети + Второй оператор серверов в приложении! + Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных. + для лучшей конфиденциальности метаданных. + Улучшенная навигация в разговоре + Посмотреть измененные условия + Устройства Xiaomi: пожалуйста, включите опцию Autostart в системных настройках для работы нотификаций.]]> + Нет сообщения + Это сообщение было удалено или еще не получено. + Сообщение слишком большое! + Пожалуйста, уменьшите размер сообщения и отправьте снова. + Пожалуйста, уменьшите размер сообщения или уберите медиа и отправьте снова. + Чтобы отправить сообщение, скопируйте и уменьшите его размер. + Поделитесь одноразовой ссылкой с другом + Поделитесь адресом + Поделитесь SimpleX адресом в социальных сетях. + Адрес SimpleX и одноразовые ссылки безопасно отправлять через любой мессенджер. + Вы можете установить имя соединения, чтобы запомнить кому Вы отправили ссылку. + Новый сервер + Создать одноразовую ссылку + Для социальных сетей + Или поделиться конфиденциально + Адрес SimpleX или одноразовая ссылка? + Настройки адреса + Добавьте сотрудников в разговор. + Бизнес адрес + end-to-end шифрованием, с пост-квантовой безопасностью в прямых разговорах.]]> + Приложение всегда выполняется в фоне + Проверять сообщения каждые 10 минут + Без фонового сервиса + Нотификации и батарейка + Как это улучшает конфиденциальность + Операторы серверов + Выберите операторов сети. + Вы можете настроить операторов в настройках Сети и серверов. + Продолжить + Посмотреть позже + Обновить + Связанные мобильные устройства + Покинуть разговор? + Вы прекратите получать сообщения в этом разговоре. История будет сохранена. + Добавить друзей + Добавить сотрудников + Удалить разговор + Удалить разговор? + Пригласить в разговор + Разговор будет удален для всех участников - это действие нельзя отменить! + Оператор + %s серверы + %s.]]> + Условия будут приняты: %s + Оператор сети + Использовать %s + Использовать серверы + %s.]]> + %s.]]> + Или импортировать файл архива + Доступная панель чата + Разговор будет удален для Вас - это действие нельзя отменить! + Покинуть разговор + Только владельцы разговора могут поменять предпочтения. + Текст условий использования не может быть показан, вы можете посмотреть их через ссылку: + Разговор + Член будет удален из разговора - это действие нельзя отменить! + Серверы по умолчанию + Роль будет изменена на %s. Все участники разговора получат уведомление. + Ваш профиль будет отправлен участникам разговора. + %s.]]> + %s.]]> + Условия использования + Дополнительные серверы файлов и медиа + Ошибка сохранения сервера + Для доставки сообщений + Открыть изменения + Оператор сервера изменен. + Протокол сервера изменен. + Серверы для новых файлов Вашего текущего профиля + Для получения + Использовать для сообщений + Размыть + Прямые сообщения между членами запрещены. + Бизнес разговоры + - Открывает разговор на первом непрочитанном сообщении.\n- Перейти к цитируемому сообщению. + Конфиденциальность для ваших покупателей. + %1$s.]]> + Разговор уже существует! + только с одним контактом - поделитесь при встрече или через любой мессенджер.]]> + Нет серверов для доставки сообщений. + Вы можете настроить серверы позже. + SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение. + Приложение защищает вашу конфиденциальность, используя разные операторы в каждом разговоре. + Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем. + Ошибка сохранения серверов + Условия будут приняты для включенных операторов через 30 дней. + Ошибка приема условий + Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети. + Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности. + Например, если ваш контакт получает сообщения через сервер SimpleX Chat, ваше приложение будет доставлять их через сервер Flux. + Прямые сообщения между членами запрещены в этом разговоре. + Группы + Удалить + Удалить список? + Все + Изменить порядок + Избранное + запрошено соединение + Редактировать + Предприятия + Включить журналы + О операторах + Ошибка при сохранении базы данных + Соединение не готово. + Ошибка обновления списка чата + Ошибка создания списка чатов + Список + Никаких чатов в списке %s. + Без непрочитанных чатов + Никаких чатов + Чаты не найдены + Все чаты будут удалены из списка %s, а сам список удален + Добавить список + Примечания + Открыто с %s + Создать список + Добавить в список + Изменить список + Сохранить список + Имя списка... + Исправить соединение? + Соединение требует повторного согласования шифрования. + Исправление + Выполняется повторное согласование шифрования. + принятое приглашение + Ошибка при загрузке списков чатов + Контакты + Название списка и эмодзи должны быть разными для всех списков. + Пожаловаться + Спам + Пожаловаться на спам: увидят только модераторы группы. + Это действие не может быть отмененено - сообщения, отправленные и полученные в этом чате ранее чем выбранное, будут удалены + Получайте уведомления от упоминаний. + Сообщения о нарушениях запрещены в этой группе. + Пожаловаться на нарушение: увидят только модераторы группы. + Установить имя чата… + Улучшенная производительность групп + Приватные названия медиафайлов. + Спам + Сообщения о нарушениях + Непрочитанные упоминания + Да + Упоминайте участников 👋 + Улучшенная приватность и безопасность + Ускорено удаление групп. + Ускорена отправка сообщений. + Помогайте администраторам модерировать их группы. + Организуйте чаты в списки + Вы можете сообщить о нарушениях + Установите время исчезания сообщений в чатах. + Вы можете упомянуть до %1$s пользователей в одном сообщении! + Причина сообщения? + Эта жалоба будет архивирована для вас. + Разрешить отправлять сообщения о нарушениях модераторам. + Содержание нарушает условия использования + Ошибка чтения пароля базы данных + сообщение о нарушении заархивировано %s + Нарушение правил группы + Неприемлемое сообщение + Другая причина + Неприемлемый профиль + %d сообщений о нарушениях + Ошибка создания сообщения о нарушении + Соединение заблокировано + Соединение заблокировано сервером оператора:\n%1$s. + Спросить + Отключить автоматическое удаление сообщений? + Удалить сообщения с вашего устройства. + Отключить удаление сообщений + по умолчанию (%s) + Все сообщения о нарушениях будут заархивированы для вас. + Архивировать все сообщения о нарушениях? + Архивировать %d сообщений о нарушениях? + Для меня + Архивировать сообщение о нарушении + Архивировать сообщения о нарушениях + Удалить сообщение о нарушении + Файл заблокирован оператором сервера:\n%1$s. + Для всех модераторов + 1 сообщение о нарушении + Измененить автоматическое удаление сообщений? + 1 год + Не пропустите важные сообщения. + Ошибка сохранения настроек + заархивированное сообщение о нарушении + архивировать + Архивировать сообщение о нарушении? + Пароль не может быть прочитан из Keystore. Это могло произойти после обновления системы, несовместимого с приложением. Если это не так, обратитесь к разработчикам. + Пароль не может быть прочитан из Keystore, пожалуйста, введите его. Это могло произойти после обновления системы, несовместимого с приложением. Если это не так, обратитесь к разработчикам. + модератор + ожидает утверждения + ожидает + Обновленные условия + Запретить жаловаться модераторам группы. + Члены группы могут пожаловаться модераторам. + Сообщения в этом чате никогда не будут удалены. + Открыть ссылку из списка чатов + Открыть веб-ссылку? + Пожаловаться на профиль: увидят только модераторы группы. + Сообщения о нарушениях + Пожаловаться: увидят только модераторы группы. + Выключить уведомления для всех + Использовать TCP-порт %1$s, когда порт не указан. + Использовать TCP-порт 443 только для серверов по умолчанию. + Все серверы + Серверы по умолчанию + Нет + Использовать веб-порт + Нет + Пожаловаться на сообщение: увидят только модераторы группы. + отклонён + Сообщение о нарушении: %s + TCP-порт для отправки сообщений + Открыть ссылку + отклонён + Только отправитель и модераторы видят это + Только вы и модераторы видят это + Разблокировать членов для всех? + Сообщения от этих членов группы будут показаны! + Все новые сообщения от этих членов группы будут скрыты! + Заблокировать членов для всех? + Члены группы будут удалены - это действие нельзя отменить! + Участники будут удалены из разговора - это действие нельзя отменить! + модераторы + Удалить членов группы? + Принять + Используя SimpleX Chat, Вы согласны:\n- отправлять только законные сообщения в публичных группах.\n- уважать других пользователей – не отправлять спам. + Частные разговоры, группы и Ваши контакты недоступны для операторов серверов. + Настроить операторов серверов + Политика конфиденциальности и условия использования. + 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 5701c0ca78..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 @@ ขอรับภาพ ขอรับวิดีโอ ข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น - เพิ่มเซิร์ฟเวอร์… + เพิ่มเซิร์ฟเวอร์ เวอร์ชันแอป เวอร์ชันแอป: v%s ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. @@ -160,7 +160,6 @@ หมดเวลาการเชื่อมต่อ เชื่อมต่อผ่านลิงค์กลุ่ม\? ผู้ติดต่อและข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! - สร้างเมื่อ %1$s ขนาดไฟล์สูงสุดที่รองรับในปัจจุบันคือ %1$s ธีมที่กำหนดเอง ID ฐานข้อมูลและตัวเลือกการแยกการส่งผ่าน @@ -239,8 +238,6 @@ ความผิดพลาดในฐานข้อมูล ยืนยันการอัพเกรดฐานข้อมูล ดาวน์เกรดฐานข้อมูล - ที่เก็บแชทถาวร - ที่เก็บแชทถาวร การแชทหยุดทํางานแล้ว เชื่อมต่อสำเร็จ กำลังเปลี่ยนที่อยู่… @@ -432,8 +429,6 @@ เวอร์ชันฐานข้อมูลใหม่กว่าแอป แต่ไม่มีการย้ายข้อมูลลงสำหรับ: %s การย้ายข้อมูลที่แตกต่างกันในแอป/ฐานข้อมูล: %s / %s ปรับลดรุ่นและเปิดแชท - ลบที่เก็บถาวร - ลบที่เก็บแชทถาวร\? กลุ่มที่ไม่ได้ใช้งาน ไม่พบกลุ่ม! คำเชิญเข้าร่วมกลุ่มหมดอายุแล้ว @@ -485,7 +480,7 @@ สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้ สมาชิกกลุ่มสามารถส่งข้อความเสียง - ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้ + ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้ ข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) เป็นสิ่งต้องห้ามในกลุ่มนี้ สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ ลบหลังจาก @@ -665,10 +660,7 @@ การตั้งค่าเครือข่าย จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ไม่ - จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ - โฮสต์หัวหอมจะถูกใช้เมื่อมี โฮสต์หัวหอมจะไม่ถูกใช้ - โฮสต์หัวหอมจะไม่ถูกใช้ รหัสผ่านที่จะแสดง สายที่ไม่ได้รับ ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น @@ -745,7 +737,7 @@ การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้! ขณะนี้ผู้ดูแลระบบสามารถ: \n- ลบข้อความของสมาชิก -\n- ปิดการใช้งานสมาชิก (บทบาท \"ผู้สังเกตการณ์\") +\n- ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")
ปฏิกิริยาต่อข้อความ นาที เดือน @@ -754,7 +746,6 @@ เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ ข้อผิดพลาดในการส่งข้อความ ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ SMP อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน - ถ้า SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร\?]]> โฮสต์หัวหอมจะถูกใช้เมื่อมี ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ การเปิดลิงก์ในเบราว์เซอร์อาจลดความเป็นส่วนตัวและความปลอดภัยของการเชื่อมต่อ ลิงก์ SimpleX ที่ไม่น่าเชื่อถือจะเป็นสีแดง @@ -776,7 +767,6 @@ ได้รับการยืนยัน… นิยามความเป็นส่วนตัวใหม่ GitHub repository ของเรา]]> - อ่านเพิ่มเติมใน GitHub repository ของเรา การแจ้งเตือนส่วนตัว โปรดรายงานไปยังผู้พัฒนาแอป ความเป็นส่วนตัวและความปลอดภัย @@ -840,7 +830,6 @@ คืนค่า คืนค่าฐานข้อมูลสำรองไหม\? กู้คืนข้อผิดพลาดของฐานข้อมูล - บันทึกไฟล์เก็บถาวร ลบแล้ว %1$s ลบคุณออกแล้ว ถูกลบแล้ว @@ -852,10 +841,8 @@ บันทึกและอัปเดตโปรไฟล์กลุ่ม กำลังรับผ่าน รีเซ็ตเป็นค่าเริ่มต้น - เปลี่ยนกลับ บันทึก รีเซ็ตสี - บันทึกสี ได้รับ, ห้าม ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์ ลดการใช้แบตเตอรี่ @@ -950,7 +937,6 @@ ติดดาวบน GitHub เปลี่ยน กำลังเริ่มต้น… - หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง รองรับบลูทูธและการปรับปรุงอื่นๆ หยุดแชร์ที่อยู่ไหม\? @@ -1025,7 +1011,7 @@ ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate! แพลตฟอร์มการส่งข้อความและแอปพลิเคชันที่ปกป้องความเป็นส่วนตัวและความปลอดภัยของคุณ การส่งข้อความส่วนตัวรุ่นต่อไป - บทบาทจะถูกเปลี่ยนเป็น \"%s\" ทุกคนในกลุ่มจะได้รับแจ้ง + บทบาทจะถูกเปลี่ยนเป็น "%s" ทุกคนในกลุ่มจะได้รับแจ้ง การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่ หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้าโปรไฟล์แชทของคุณ การแยกการขนส่ง @@ -1148,7 +1134,6 @@ ใช้พร็อกซี SOCKS ใช้การเชื่อมต่ออินเทอร์เน็ตโดยตรงหรือไม่\? ใช้พร็อกซี SOCKS หรือไม่\? - อัปเดตการตั้งค่าโฮสต์ .onion ไหม\? ใช้โฮสต์ .onion เมื่อพร้อมใช้งาน อัปเดตโหมดการแยกการขนส่งไหม\? @@ -1161,7 +1146,6 @@ โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ: รอคำตอบ… - รับข้อความและผู้ติดต่อของคุณ – เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา]]> ใช้แชท การสนทนาทางวิดีโอ การโทรของคุณ @@ -1204,7 +1188,7 @@ ข้อความต้อนรับ คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ %s - บทบาทจะถูกเปลี่ยนเป็น \"%s\" สมาชิกจะได้รับคำเชิญใหม่ + บทบาทจะถูกเปลี่ยนเป็น "%s" สมาชิกจะได้รับคำเชิญใหม่ ข้อความต้อนรับ โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม อัปเดต @@ -1344,4 +1328,4 @@ ในการตอบกลับถึง ไม่มีประวัติ encryptionใช้ได้ - \ No newline at end of file + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 9ae8707c11..257daec596 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -22,7 +22,7 @@ Görünüm Ayarlarınız Sessize al - Sessizden çıkar + Susturmayı kaldır İptal Adres değişikliğini iptal et\? 30 saniye @@ -38,7 +38,7 @@ Adres değişikliği iptal edilecek. Eski alıcı adresi kullanılacaktır. 1 dakika Tüm mesajlar silinecektir. Bu, geri alınamaz! Mesajlar, YALNIZCA senin için silinecektir. - Sunucu ekle… + Sunucu ekle Veri tabanı ayarları tek kullanımlık bağlantı Gelişmiş ağ ayarları @@ -71,8 +71,7 @@ İzin ver Uygulama erişim kodu Uygulamadaki her konuşma profliniz için ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.]]> - Konuştuğun kişilerin ve grup üyelerinin tamamı için ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır. -\nBilgin olsun: Ç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. + Konuştuğun kişilerin ve grup üyelerinin tamamı için ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.\nBilgin olsun: Ç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.]]> Kaydet ve grup üyelerini bilgilendir Uygulama açıkken çalışır Ara @@ -87,14 +86,14 @@ Konuştuğunuz kişinin uygulamasından güvenlik kodunu okut. WebRTC ICE sunucu adreslerinin doğru formatta olduğundan emin olun: Satırlara ayrılmış ve yinelenmemiş şekilde. Kaydet - TEMA RENKLERİ + ARAYÜZ RENKLERİ Otomatik-kabul ayarlarını kaydet Ayarlar kaydedilsin mi? Kaydet ve konuştuğun kişilere bildir Tercihleri kaydet\? Profil parolasını kaydet Profil sadece konuştuğun kişilerle paylaşılır. - Gizli iletişimin gelecek kuşağı + Mesajlaşmanın geleceği Ses kapalı Doğrulama iptal edildi Yeniden başlat @@ -106,7 +105,6 @@ Veri tabanı yedeğini geri yükle\? Veri tabanını geri yüklerken hata Veritabanı sürüm düşürme - Arşivi kaydet %s (mevcut) Kaydet ve grup profilini güncelle Karşılama mesajı kaydedilsin mi? @@ -122,7 +120,6 @@ SimpleX Koyu tema Tema - Rengi kaydet Temayı içe aktar Temayı içe aktarırken hata oluştu 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. @@ -139,18 +136,18 @@ Kaydet ve kişiyi bilgilendir Parolayı, Keystore\'a kaydet. Ses açık - Yetki, \"%s\" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek. + Yetki, "%s" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek. Sesli/görüntülü aramalar yasaktır. Kimlik doğrulama başarısız SimpleX Adresi Mesaj, tüm üyeler için silinecek. Gizliliğinizi ve güvenliğinizi koruyan mesajlaşma ve uygulama platformu. Gizlilik kipi - Yetki, \"%s\" olarak değiştirilecek. Üye, yeni bir davet alacak. + Yetki, "%s" olarak değiştirilecek. Üye, yeni bir davet alacak. Yetki Sesli mesajlara izin verilsin mi? Profil ekle - Üyelere direkt mesaj gönderilmesine izin ver. + Üyelere doğrudan mesaj gönderilmesine izin ver. Kendiliğinden yok olan mesajlar göndermeye izin ver. Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde) Dosya ve medya göndermeye izin ver. @@ -267,7 +264,7 @@ Kullanıldığında bütün veriler silinir. Kendiliğinden yok olan mesajlar Kişilerinin sana, kendiğinden yok olan mesajlar göndermesine izin ver. - Bu grupta kendiliğinden yok olan mesajlara izin verilmiyor. + Kendiliğinden yok olan mesajlara izin verilmiyor. %1$d mesajlar deşifrelenemedi. %1$s ÜYELER %1$d atlanılmış mesaj(lar) @@ -361,7 +358,7 @@ %d saat Yeni yöneticiler artık: \n- üyelerin mesajlarını silebilir. -\n- üyeleri etkisizleştirebilir (\"gözlemci\" yetkisi verir) +\n- üyeleri etkisizleştirebilir ("gözlemci" yetkisi verir)
Konuşmalar Grup bağlantısı oluştur Konuşma isteğini onaylarken hata oluştu @@ -430,9 +427,6 @@ Veri tabanı, rastgele bir parola ile şifrelendi. Dışa aktarmadan önce lütfen değiştir. Dosyaları ve medyayı sil\? Veri tabanı şifrelenecektir. - %1$s tarihinde oluşturuldu - Belgeliği sil - Konuşma belgeliğini sil\? %s üyesinin yetkisi %s olarak değiştirildi silinmiş grup kendi yetkini, %s olarak değiştirdin @@ -506,7 +500,7 @@ Kendiliğinden şu sürede yok olacak Kendiliğinden şu sürede yok olacak: %s etkin - Direkt mesaj + Doğrudan mesajlar Devre dışı bırak Görünen ad, boşluk gibi aralıklama türleri içeremez. İsmini gir: @@ -530,7 +524,7 @@ %s üyesi için şifreleme kabul edildi doğrudan Yeniden gösterme - Bu grupta üyeler arası direkt mesajlar yasaklıdır. + Bu grupta üyeler arası doğrudan mesajlaşma yasaklıdır. konuşulan kişi için etkinleşti senin için etkinleştirildi %d sn @@ -593,7 +587,7 @@ Gizli profil parolası Markdown nasıl kullanılır Nasıl çalışıyor - Kötüye kullanmaya ve istenmeyen mesajlara duyarlı + Spamdan etkilenmez Çağırıyı bitir. Kameranın karşı yüzüne geç Konuşma veri tabanı içe aktarılsın mı? @@ -611,7 +605,7 @@ Yedekleri geri yükledikten sonra şifrelemeyi onar. Fransız arayüzü Daha da azaltılmış pil kullanımı - Grup üyeleri, mesajlara tepki ekleyebilir. + Üyeler, mesajlara tepki ekleyebilir. Grup profili, üyelerinin aygıtlarında barındırılmaktadır, sunucularda değil. Gizle Gizle @@ -653,10 +647,10 @@ Grup adını gir: Grup tam adı: Dosya ve medya - Grup üyeleri direkt mesaj gönderebilir. - Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) + Grup üyeleri doğrudan mesaj gönderebilir. + Üyeler, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) Grup üyeleri sesli mesaj gönderebilirler. - Bu toplu konuşmada, dosya ve medya yasaklanmıştır. + Dosya ve medya yasaklanmıştır. Grup üyeleri dosya ve medya paylaşabilir. Grup bağlantıları Konuşmada devre dışı bırakıldığında bile @@ -696,7 +690,7 @@ Android Keystore parolayı güvenli bir şekilde saklamak için kullanılır - bildirim hizmetinin çalışmasını sağlar. Karşılama mesajı Karşılama mesajı - Bu grupta sesli mesajlar yasaktır. + Sesli mesajlar yasaktır. Neler yeni %s sürümünde yeni Daha fazla bilgi edinin @@ -710,7 +704,7 @@ Anlık bildirimler Anlık bildirimler Anlık bildirimler devre dışı! - SimpleX in arkaplanda çalışmasına izin ver seçeneğini işaretleyin. Aksi takdirde, bildirimler devre dışı bırakılacaktır.]]> + izin verin. ]]> Kişi ismi Cihaz doğrulaması devre dışı. SimpleX Kilidi Kapatılıyor. Cihaz doğrulaması etkin değil. Cihaz doğrulamasını etkinleştirdikten sonra SimpleX Kilidini Ayarlar üzerinden açabilirsiniz. @@ -736,13 +730,11 @@ Arkadaşlarınızı davet edin kalın italik - Daha sonra ayarlardan değiştirebilirsiniz. + Pili nasıl etkiler kişi uçtan uca şifrelemeye sahiptir kişi uçtan uca şifrelemeye sahip değildir Sohbet durduruldu Aklınızda bulunsun: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]> - Sohbet arşivi - SOHBET ARŞİVİ %1$s grubuna davet Gruba katıl\? %1$s davet edildi @@ -758,7 +750,7 @@ aramaya bağlanılıyor… Gönderildi bilgisi kapalı! Birkaç şey daha - Daha fazla pil kullanır! Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.]]> + Daha fazla pil kullanır! Uygulama her zaman arka planda çalışır - bildirimler anında gösterilir.]]> Çok yakında! Kişi ve tüm mesajlar silinecektir - bu geri alınamaz! Kişi henüz bağlanmadı! @@ -766,7 +758,7 @@ Japonca ve Portekizce kullanıcı arayüzü Kişiler davet edilemiyor! Davetin süresi dolmuş! - Geri alınamaz mesaj silme bu grupta yasaktır + Geri alınamaz mesaj silme yasaktır Mesaj gönderildi bilgisi! Gruba katılınıyor tek seferlik gizli bağlantı paylaştınız @@ -826,7 +818,7 @@ Okundu olarak işaretle Okunmadı olarak işaretle Sohbet konsolu - Pil için iyi. Arka plan hizmeti mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.]]> + Pil ömrü için iyi. Uygulama mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.]]> Güvenlik kodlarını kişilerinizle karşılaştırın. Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir. Pil için en iyisi. Sadece uygulama çalışırken bildirim alırsınız (arka plan hizmeti YOK).]]> @@ -843,7 +835,7 @@ Alıcı adresini değiştir Mesaj tepkileri Tercihleriniz - Mesaj tepkileri bu grupta yasaklıdır + Mesaj tepkileri yasaklıdır. Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız. Zaten %1$s e bağlısınız Doğrulanamadınız; lütfen tekrar deneyin. @@ -858,7 +850,6 @@ ICE sunucularınız Nasıl Mevcut profiliniz - alınacağını siz kontrol edersiniz, kişileriniz - onlara mesaj göndermek için kullandığınız sunucular.]]> video arama (uçtan uca şifreli değil) ICE sunucularınız Video kapalı @@ -915,7 +906,7 @@ TAMAM Daha fazla Tek seferlik davet bağlantısı - Ağ ayarları + Gelişmiş ayarlar Siz uygulamayı yeniden başlatana kadar bildirimler çalışmayacaktır Kapalı Yeni görünen ad: @@ -940,7 +931,7 @@ - daha stabil mesaj iletimi. \n- biraz daha iyi gruplar. \n- ve daha fazlası! - 2 katmanlı uçtan uca şifreleme ile kullanıcı profillerini, kişileri, grupları ve gönderilen mesajları depolar.]]> + Sadece istemci cihazlar kullanıcı profillerini, kişileri, grupları ve gönderilen mesajları depolar. Grup tercihlerini sadece grup sahipleri değiştirebilir. metin yok Ağ durumu @@ -949,7 +940,7 @@ Kilit modunu değiştir Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor. 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. - SimpleX arka plan hizmeti kullanılır - günde pilin yüzde birkaçını kullanır.]]> + SimpleX arka planda çalışır.]]> Periyodik bildirimler Periyodik bildirimler devre dışı Bildirimleri almak için lütfen veri tabanı parolasını girin @@ -958,7 +949,7 @@ Hatırlayın veya güvenli bir şekilde saklayın - kaybolan bir parolayı kurtarmanın bir yolu yoktur! Sohbet konsolunu aç - Sohbet profillerini aö + Sohbet profillerini değiştir. Bu metin ayarlarda mevcut Filtrelenmiş sohbet yok Çok fazla görsel! @@ -990,9 +981,8 @@ Zaman dilimi, görsel/ses korumak için UTC kullan. Özel dosya adları Yeni bir sohbet başlatmak için - İnsanlar size sadece paylaştığınız bağlantılar üzerinden ulaşabilir. + Kimin bağlanabileceğine siz karar verirsiniz. Gizlilik yeniden tanımlanıyor - GitHub repomuzda daha fazlasını okuyun. Periyodik Gizli bildirimler Aldığın bağlantıyı yapıştır @@ -1007,7 +997,7 @@ Mesaj tepkilerini yasakla. Sesli mesaj göndermeyi yasakla. Geri alınamaz mesaj silme işlemini yasakla. - Üyelere direkt mesaj göndermeyi yasakla. + Üyelere doğrudan mesaj göndermeyi yasakla. Dosya ve medya göndermeyi yasakla. Canlı mesajlar Arayüz geliştirildi @@ -1032,7 +1022,7 @@ Mesaj taslağı Sohbet profillerini parola ile koru! Daha az pil kullanımı - 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. + Gizliliği korumak için, SimpleX her bir konuşma için farklı bir ID kullanır. Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s). Alıcılar güncellemeleri siz yazdıkça görürler. Bilgilerinizi kullanarak giriş yapın @@ -1044,7 +1034,6 @@ Dosyayı durdur Hata ProfilProfil oluştur - Veri tabanı eylemlerini etkinleştirmek için sohbeti durdur. Dosya göndermeyi durdur? Sohbeti durdur Mevcut profili kullan @@ -1110,7 +1099,7 @@ Kişiyi ve mesajı göster Daha iyi gruplar Videonun kodu çözülemiyor. Lütfen farklı bir video deneyin veya geliştiricilerle iletişime geçin. - İçe aktarma sırasında bir takım hatlar oluştu - daha fazla detay için sohbet konsoluna bakabilirsiniz. + İçe aktarma sırasında bazı önemli olmayan hatalar oluştu: Geliştirici seçeneklerini göster %s bağlandı Onaylarsanız, mesajlaşma sunucuları IP adresinizi ve sağlayıcınızı - hangi sunuculara bağlandığınızı - görebilecektir. @@ -1146,7 +1135,6 @@ Bağlantı paylaş SimpleX Ekibi %s, %s ve %s bağlandı - Geri al SOCKS VEKİLİ Masaüstür cihazlar SMP sunucuları @@ -1190,7 +1178,7 @@ SimpleX Kilit modu Dosya paylaş… 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. - Herhangi bir kullanıcı tanımlayıcısı olmayan ilk platform - tasarım gereği gizli. + Herhangi bir kullanıcı tanımlayıcısı yok. Hoparlör kapalı Şifreleme çalışıyor ve yeni bir şifreleme anlaşması gerekli değil. Yoksa bağlantı hataları ortaya çıkabilir! Göster @@ -1213,10 +1201,10 @@ Mobilden tara Bağlantıları onayla Mesaj paylaş… - Önceki mesajın hash\'i farklı. + Önceki mesajın hash\'i farklı.\" SimpleX Kilit aktif değil! SimpleX Kilit - Direkt bağlanılsın mı? + Doğrudan bağlanılsın mı? Bu ayarlar mevcut profiliniz içindir Sunucu testi başarısız! Bağlantıyı onayla @@ -1231,7 +1219,7 @@ Bluetooth desteği ve diğer iyileştirmeler. Ayarlar AYARLAR - Bağlanmak için direkt mesaj gönderin + Bağlanmak için doğrudan mesaj gönderin Güvenlik kodu Daha hızlı gruplara katılma ve daha güvenilir mesajlar. Sohbet profiliniz grup üyelerine gönderilecek @@ -1319,14 +1307,12 @@ Kayıt %s te güncellendi Uygulama yeni yerel dosyaları şifreler (videolar dışında). %s , %s de - Bağlantı için Onion ana bilgisayarları gerekli olacaktır. Alıcılar devre dışı bırakılsın mı? Bağlantı yeniden senkronizasyonunu şifrele? Grup ayarlarından ve kişilerden geçersiz kılınmış olabilirler Yeni üyelere geçmiş gönderilmedi. Güvenlik değerlendirmesi Yeniden dene - Onion ana bilgisayarları mümkün olduğunda kullanılacaktır. 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. İstenmeyen mesajları gizlemek için. Test %s adımında hata yaşandı. @@ -1363,7 +1349,6 @@ Ayarlardaki parola silinsin mi? Alıcılar etkinleştirilsin mi? Yeni bir sohbet başlatmak için tıkla - Onion ana bilgisayarları kullanılmayacaktır. Kişinin engelini kaldır Yönlendirici sunucusu sadece lazım ise kullanılacak. Diğer taraf IP adresini görebilir. %s ın bağlantısı kesildi]]> @@ -1404,7 +1389,7 @@ Yapıştırdığın bağlantı bir SimpleX bağlantısı değil. Gönderilmiş mesaj Gruplar için alıcılar devre dışı bırakılsın mı? - Aktarıcı sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. + Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. Dosya yükleniyor Masaüstüne bağlanıyor Eklenecek kişi yok @@ -1473,7 +1458,6 @@ Bir sonraki mesajın kimliği yanlış (bir öncekinden az veya aynı). \nBazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Kişi gizlendi: - eğer SimpleX’in hiç kullanıcı tanımlayıcısı yok, nasıl mesajları gönderiyor? ]]> Ş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. @@ -1511,7 +1495,6 @@ Telefona bağlandı Bağlanırken takma ada geçiş yap. Mesaj gönderildi! - .onion ana bilgisayarları ayarı güncellensin mi? Yeni bağlantılar için kullan senin için adres değiştirildi SOCKS vekilini kullan? @@ -1694,7 +1677,7 @@ Uyarı: Birden fazla cihazda sohbet başlatmak desteklenmez ve mesaj iletimi başarısızlıklara neden olabilir. Veritabanı parolasını doğrulayın Parolayı doğrulayın - Tüm kişileriniz, konuşmalarınız ve dosyalarınız güvenli bir şekilde şifrelenir ve yapılandırılmış XFTP rölelerine parçalar halinde yüklenir. + 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. Arşivle ve yükle Uyarı: arşiv silinecektir.]]> Taşımak için veritabanı parolasını hatırladığınızı doğrulayın. @@ -1730,7 +1713,7 @@ SimpleX bağlantısı gönderimini yasakla Eşzamanlılık alınıyor SimpleX bağlantıları - SimpleX bağlantıları bu grupta yasaklandı + SimpleX bağlantıları yasaklandı Kulaklık Hoparlör Kulaklıklar @@ -1770,4 +1753,474 @@ Profil resimleri Profil resimlerini şekillendir Kare,daire, veya aralarında herhangi bir şey. - \ No newline at end of file + Kapasite aşıldı - alıcı önceden gönderilen mesajları almadı. + Hedef sunucu hatası: %1$s + Hata: %1$s + Yönlendirme sunucusu: %1$s +\nHedef sunucu hatası: %2$s + Yönlendirme sunucusu: %1$s +\nHata: %2$s + Mesaj iletimi uyarısı + Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu. + Sunucu adresi ağ ayarlarıyla uyumlu değil. + Sunucu sürümü ağ ayarlarıyla uyumlu değil. + Yanlış anahtar veya bilinmeyen bağlantı - büyük olasılıkla bu bağlantı silinmiştir. + Gizli yönlendirme + Bilinmeyen sunucular + Her zaman gizli yönlendirmeyi kullan. + Gizli yönlendirmeyi KULLANMA. + Mesaj yönlendirme modu + Hiçbir zaman + Bilinmeyen sunucularla gizli yönlendirme kullan. + Sürüm düşürmeye izin ver + Hayır + Mesaj yönlendirme yedeklemesi + Her zaman + Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN. + IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin. + Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin. + GİZLİ MESAJ YÖNLENDİRME + Mesaj durumunu göster + IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır. + Korumasız + IP adresi korunmadığında bilinmeyen sunucularla gizli yönlendirme kullan. + IP gizliyken + Evet + Sohbet teması + Profil teması + Siyah + Renk modu + Karanlık mod renkleri + Aydınlık + Sistem + Ek vurgu 2 + Bütün renk modları + Şuna uygula + Karanlık mod + Doldur + Ölçeklendir + Gönderilen cevap + Varsayılan temaya ayarla + Gelişmiş ayarlar + Günaydın! + Karanlık + Aydınlık mod + IP adresini koru + DOSYALAR + Sohbet renkleri + Sığdır + Alınan cevap + İyi öğlenler! + Resmi kaldır + Tekrarla + Rengi sıfırla + Sohbet listesini yeni pencerede göster + Bilinmeyen sunucular! + Tor veya VPN olmadan, IP adresiniz bu XFTP yönlendiricileri tarafından görülebilir: +\n%1$s. + Tor veya VPN olmadan, IP adresiniz dosya sunucularına görülebilir. + Duvar kağıdı vurgusu + Duvar kağıdı arkaplanı + Uygulama, bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion veya SOCKS vekilleri etkin değilse). + 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 + Sohbetlerinizin farklı görünmesini sağlayın! + Farsça Arayüz + Kullanıcı temasına sıfırla + IP adresinizi kişileriniz tarafından seçilen mesajlaşma yönlendiricilerinden koruyun. +\n*Ağ ve sunucular* ayarlarında etkinleştirin. + Gizli mesaj yönlendirme 🚀 + Bilinmeyen sunuculardan gelen dosyaları onayla. + Geliştirilmiş mesaj iletimi + Yeni sohbet temaları + Dosyaları güvenle alın + Azaltılmış pil kullanımı ile. + Uygulama teması + Uygulama temasına sıfırla + Hata ayıklama teslimatı + Mesaj kuyruğu bilgisi + hiçbiri + sunucu kuyruk bilgisi: %1$s +\n +\nson alınan msj: %2$s + Yönlendirme sunucusuna (%1$s) bağlantı sırasında hata oluştu. Lütfen daha sonra tekrar deneyin. + Dosya sunucusu hatası. %1$s + Tara / Bağlantı yapıştır + Güncellemeleri kontrol et + Devre dışı + Uygulama Güncellemesi indirildi + Daha sonra hatırlat + Güncellemeleri kontrol et + Mesajı göndermeye zorlamak için sunucuya yeniden bağlan. Bu ekstra internet kullanır. + Hata + Mesajların yönlendirilmesi sırasında hata oluştu. + Dosya bulunamadı - muhtemelen dosya silindi veya göderim iptal edildi. + %1$s Mesaj yönlendirilsin mi ? + Hiçbir şey seçilmedi. + Yönlendirilecek bir şey yok! + %d seçildi. + Mesajlar dosyalar olmadan iletilsin mi ? + Mesajlar siz seçtikten sonra silindi. + Mesaj gönderilemedi + İndir + Mesajları ilet.. + %1$s mesaj iletiliyor + %1$s Mesaj kayıt ediliyor. + bağlan + Ara + mesaj + Sadece sohbeti sil + açık + ara + Profil değiştirme sırasında hata oluştu. + Sohbet profili seç + XFTP sunucuları yapılandırıldı + Medya ve dosya sunucuları + Kimlik bilgilerini proxy ile kullanmayın. + Proxy kayıt edilirken hata oluştu. + Proxy konfigürasyonunun doğru olduğundan emin olun. + Şifre + Beta + Uygulama güncellemesi indiriliyor, uygulamayı kapatmayın + Kurulum başarılı + Güncellemeyi Kur + Dosya konumunu aç + Devre dışı bırak + Lütfen uygulamayı yeniden başlatın. + Davet + Yarat + Kapalı + Mesajlaşma Veritabanı + Devam et + Mesaj durumu: %s + Yazı tipi boyutu + Yeni bir sohbet deneyimi 🎉 + Ağınızı kontrol edin + Bağlantı ve sunucuların durumu + Yazı boyutunu arttır + 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. + Tüm istatistikler sıfırlansın mı ? + Hata, sunucuya yeniden bağlanılıyor + Tüm sunuculara yeniden bağlan + Kopyalar + Parçalar yüklendi + Dosyalar İndirildi + İndirme hataları + Mesaj durumu + Geçersiz link + Lütfen SimpleX bağlantısının doğru olup olmadığını kontrol edin. + Varış sunucusu ardesi (%1$s) yönlendirme sunucusu (%2$s) ile uyumsuz. + Varış sunucusu sürümü (%1$s) yönlendirme sunucusu (%2$s) ile uyumsuz. + Mesaj iletildi + Kullanıcı aktif olursa mesaj iletilebilir. + Mesaj + Arşivli kişiler + Filtrelenmiş kişiler yok + Bağlantıyı yapıştır + Davet et + Devre dışı + inaktif + Aramalara izin verilsin mi ? + Aramalara izin verilmiyor + Kişi silindi. + Grup üyesine mesaj gönderilemiyor + Çağrıları aktif etmek için mesaj gönder. + Daha sonra görüşmek için kişileri arşivleyin + IP adresinizi ve bağlantılarınızı korur. + Yeni medya seçenekleri + Daha iyi gizlilik için bulanıklaştır. + Arkadaşlarınıza daha hızlı bağlanın + Tek seferde en fazla 20 mesaj silin. + Sohbet listesinden oynat. + Arşiv kaldırılsın mı ? + Bağlandı + Aktif profil + Dosyalar + Bilgi yok, yenilemeyi deneyin + Bağlanıyor + Hatalar + Mesajlar alındı + Mesajlar gönderildi + Mesaj iletimine zorlamak için tüm sunuculara yeniden bağlan. Bu ekstra internet kullanılır. + Sunucuya yeniden bağlansın mı ? + Sunuculara yeniden bağlanılsın mı ? + Hata sunuculara yeniden bağlanılıyor + Sıfırla + Bağlantılar + Yaratıldı + Şifre çözme hataları + diğer hatalar + Tamamlandı + Silindi + Silme hatası + Sunucu ayarlarını aç + Dosya durumu: %s + Yeni mesaj + Diğer SMP sunucuları + Yeniden bağlan + Direkt gönderildi. + Tüm Profiller + Grup üyesi aranamıyor + Veritabanı dışa aktarıldı + Parçalar silindi + Parçalar indirildi + Bağlı sunucular + Kişiye bağlanılıyor, lütfen bekleyin ya da daha sonra kontrol edin. + Kopyalama hatası + Bildirim göndermeden sil + Yeni versiyonları GitHub\'dan indirin + Hata istatistikler sıfırlanıyor + Yönlendirme sunucu adresi (%1$s) ağ ayarlarıyla uyumsuz. + Dosya durumu + Yönlendirme sunucusu (%1$s) varış sunucusuna (%2$s) bağlanamadı. Lütfen daha sonra tekrar deneyin. + Yönlendirme suncusu sürümü ağ ayarlarıyla uyumsuz: %1$s + Bekliyor + Lütfen kişinizden çağrılara izin vermesini isteyin. + Önceden bağlanılmış sunucular + Kişi aranamıyor + Kayıt et ve yeniden bağlan + Mesajlar silinecek - bu geri alınamaz! + Mesajlar silinmek üzere işaretlendi. Alıcı (lar) bu mesajları görebilecek. + Üylerin %d mesajı silinsin mi ? + Üye inaktif + Henüz direkt bağlantı yok mesaj admin tarafından yönlendirildi. + Mesaj sunucuları + SMP sunucları yapılandırıldı + Diğer XFTP sunucuları + Medyayı bulanıklaştır. + Kişiler silinecek - bu geri alınamaz ! + Sohbeti sakla. + Kişiyi silmek istediğinizden emin misiniz ? + Sohbet silindi! + Kişiler silindi! + Lütfen sonra tekrar deneyin + Gizli yönlendirme hatası + Dosya hatası + Detaylar + Süresi dolmuş + Gönderme hataları + denemeler + Detaylı istatistikler + İndirildi + diğer + Alınan mesajlar + Toplam alınan + Seç + Seçilen sohbet tercihleri bu mesajı yasakladı. + Tüm istatistikleri sıfırla + Tüm ip uçlarını sıfırla + %1$d dosya hata(ları)\n%2$s + %1$d dosya(lar) hala indiriliyor. + %1$d dosyası (ları) indirilemedi. + %1$d dosyası (ları) silindi. + %1$d diğer dosya hatası(ları). + %1$d dosyası (ları) indirilemedi. + %1$s mesajları iletilemedi + Orta + Mesaj alındısı + Onaylandı + WebView başlatılırken hata oluştu. WebView\'in yüklü olduğundan ve desteklenen mimarisinin arm64 olduğundan emin olun.\nHata: %s + Uygulamayı her başlattığınızda yeni SOCKS kimlik bilgileri kullanılacaktır. + Her sunucu için yeni SOCKS kimlik bilgileri kullanılacaktır. + İndir %s (%s) + Mesaj şekli + Yüzdeyi göster + Güncelleme indirme işlemi iptal edildi + Abone olurken hata + Mesajlar tüm üyeler için silinecektir. + Mesajlar tüm üyeler için moderasyonlu olarak işaretlenecektir. + Yanlış anahtar veya bilinmeyen dosya yığın adresi - büyük olasılıkla dosya silinmiştir. + Ayarlar + Profil paylaş + Bağlantınız %s\'ye taşındı ancak sizi profile yönlendirirken beklenmedik bir hata oluştu. + Proxy kimlik doğrulaması + Rastgele kimlik bilgileri kullan + Her bağlantı için farklı proxy kimlik bilgileri kullan. + Her profil için farklı proxy kimlik bilgileri kullan. + Kimlik bilgileriniz şifrelenmeden gönderilebilir. + Kullanıcı Adı + Bu sürümü atlayın + Yeni sürümlerden haberdar olmak için Kararlı veya Beta sürümleri için periyodik kontrolü açın. + Yumuşak + Bazı dosya(lar) dışa aktarılmadı + Zoom + Uygulamayı otomatik olarak yükselt + Yüklenen dosyalar + Boyut + Abone olundu + Abonelikler göz ardı edildi + Bağlantılarınız + Ses kapatıldı + Yüklendi + Sunucu istatistikleri sıfırlanacaktır - bu geri alınamaz! + Erişilebilir uygulama araç çubukları + Görünüm ayarlarından değiştirebilirsiniz. + Sohbet listesini değiştir: + Sistem modu + Erişilebilir sohbet araç çubuğu + İçin bilgi gösteriliyor + İstatistikler + %s\'den başlayarak.\nTüm veriler cihazınızda gizli tutulur. + Bir proxy aracılığıyla gönderildi + Sunucu adresi + Yükleme hataları + TCP bağlantısı + SOCKS proxy + Proxy sunucuları + Geçici dosya hatası + Video + Arşivlenen kişilerden %1$s\'e mesaj gönderebilirsiniz. + Sohbetler listesinde %1$s ile yapılan konuşmayı hala görüntüleyebilirsiniz. + XFTP sunucusu + Alım sırasında hata + SMP sunucusu + Sunucu adresi ağ ayarlarıyla uyumsuz: %1$s. + Sunucu sürümü uygulamanızla uyumlu değil: %1$s. + Kendiniz arayabilmeniz için önce irtibat kişinizin sizi aramasına izin vermelisiniz. + %s\'den başlayarak. + Onay hataları + Güvenli + Uygulama oturumu + Sunucu + Stabil + Güncelleme mevcut: %s + Bu bağlantı başka bir mobil cihazda kullanıldı, lütfen masaüstünde yeni bir bağlantı oluşturun. + Mikrofon kullanımına izin vermek için adres alanının yanındaki bilgi düğmesine tıklayın. + Safari Ayarları / Web Siteleri / Mikrofon\'u açın, ardından localhost için İzin Ver\'i seçin. + Arama yapmak için mikrofonunuzu kullanmanıza izin verin. Aramayı sonlandırın ve tekrar aramayı deneyin. + Konuşma balonu + Güçlü + Uygulamayı tek elle kullan. + Sunucu bilgileri + Toplam + Bu sunuculara bağlı değilsiniz. Mesajları onlara iletmek için özel yönlendirme kullanılır. + Aktif bağlantılar + Köşeleri yuvarlama + Yüklenen veritabanı arşivi sunuculardan kalıcı olarak kaldırılacaktır. + Proxyli + Taşıma oturumları + Dışa aktarılan veritabanını taşıyabilirsiniz. + Dışa aktarılan arşivi kaydedebilirsiniz. + Gönderilen tüm mesajların toplamı + Gönderilen mesajlar + Daha iyi mesaj tarihleri. + Özelleştirilebilir mesaj şekli. + Aynı anda en fazla 20 mesaj iletin. + Görüşme sırasında ses ve görüntüyü değiştirin. + Sohbet profilini 1 kerelik davetler için değiştirin. + Daha iyi aramalar + Daha iyi kullanıcı deneyimi + 200\'e kadar mesajı silin veya düzenleyin. + Daha iyi güvenlik ✅ + SimpleX protokolleri Trail of Bits tarafından incelenmiştir. + Adres mi yoksa tek seferlik bağlantı mı? + Mesaj sunucuları eklendi + Koşulları kabul edin + Kabul edilen koşullar + Medya ve dosya sunucuları eklendi + Uygulama araç çubukları + Adres ayarları + Bulanıklık + sadece bir kişiyle kullanılabilir - yüz yüze veya herhangi bir mesajlaşma programı aracılığıyla paylaşın]]> + %s.]]> + %s.]]> + Arkadaş ekle + İş konuşmaları + Takım üyesi ekle + kabul edilmiş davet + İş adresi + Takım üyelerini konuşmalara ekle + Uygulama her zaman arka planda çalışır + Operatörler hakkında + a+b + Liste ekle + Hepsi + Listeye ekle + %s listesindeki bütün sohbetler kaldırılacak ve liste silinecek + İşletmeler + arşivlenmiş rapor + Başka bir sebep + Raporu arşivle + %s tarafından arşivlenen rapor + 1 rapor + %s.]]> + Sor + 1 yıl + Xiaomi cihazları: Bildirimlerin çalışması için lütfen sistem ayarlarında Otomatik Başlat\'ı etkinleştirin.]]> + Tüm raporlar sizin için arşivlenecektir. + Tüm raporlar arşivlensin mi? + Raporları arşivle + %s.]]> + %s operatörü için de geçerli olacaktır.]]> + %s operatörü(leri) için de geçerli olacaktır.]]> + Mesajların moderatörlere bildirilmesine izin verin. + %d raporu arşivleyelim mi? + Arşiv + Raporu arşivleyelim mi? + uçtan uca şifreli olarak gönderilir ve doğrudan mesajlarda kuantum sonrası güvenlik sağlanır.]]> + Veritabanı şifresini okurken hata oluştu + Sohbeti sil + Sohbet silinsin mi? + Bağlantı hazır değil. + Kullanım şartları + daha iyi üstveri gizliliği için. + Sunucuları kaydederken hata oluştu + Şartlar 30 gün sonra etkin operatörler için kabul edilecektir. + Raporu sil + Listeyi değiştir + %s için de geçerli olacaktır.]]> + Üyeler herkes için engellensin mi? + Topluluk kurallarının ihlali + Şartları kabul ederken hata oluştu + Sil + Liste silinsin mi? + Bağlantı güvenliği + İçerik kullanım şartlarını ihlal ediyor + Rapor oluşturulurken hata oluştu + Bağlantı engellendi + Bağlantı sunucu operatörü tarafından engellendi:\n%1$s. + Dosya sunucu operatörü tarafından engellendi:\n%1$s. + %d rapor + Mesaj çok büyük! + Otomatik silinen mesajlar değiştirilsin mi? + Otomatik silinen mesajlar devre dışı bırakılsın mı? + Liste oluştur + Sırayı değiştir + Düzenle + Sosyal medya için + Tek kullanımlık bağlantı oluştur + Örneğin, eğer kişiniz SimpleX Sohbet sunucusundan mesajları alıyorsa, uygulamanız bu mesajları Flux sunucusundan iletecektir. + varsayılan (%s) + Bu üyelerden gelen yeni mesajların hepsi gizlenecektir. + İyileştirilmiş grup performansı + Sohbet zaten var! + %1$s ile bağlısınız.]]> + Konuşma profili %s için: + Üyeler arası doğrudan mesajlaşma yasaklıdır. + Şartlar %s tarihinde etkin operatörler için otomatik olarak kabul edilecektir. + Sohbet mesajlarını cihazınızdan silin. + Her 10 dakikada mesajları kontrol et + Devam et + İyileştirilmiş gizlilik ve güvenlik + Daha hızlı mesaj gönderme. + Bu sohbette üyeler arası doğrudan mesajlaşma yasaklıdır. + Sohbet listesi yüklenirken hata oluştu + Sohbet listesi oluşturulurken hata oluştu + Sohbet listesini güncellerken hata oluştu + Sık kullanılanlar + Konuşmalar + Veritabanını kaydederken hata oluştu + Sohbet sizin için silinecek - bu geri alınamaz! + %s için de geçerli olacaktır.]]> + Bütün moderatörler için + Benim için + Ayarlar kaydedilirken hata oluştu + Günlükleri etkinleştir + Sohbet + Grupların daha hızlı silinmesi. + Sohbet bütün üyeler için silinecek - bu geri alınamaz! + Sunucu eklerken hata oluştu + Onar + Bağlantı onarılsın mı? + Sunucuyu güncellerken hata oluştu + 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 948f91b89a..549cb01b63 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -18,8 +18,8 @@ 1-разове посилання Про SimpleX Chat Додавайте сервери, скануючи QR-коди. - Всі чати і повідомлення будуть видалені - цю дію неможливо скасувати! - Дозволяйте дзвінки тільки в разі дозволу вашого контакту. + Усі чати та повідомлення будуть видалені - цю дію неможливо скасувати! + Дозволити дзвінки тільки за умови, що ваш контакт дозволяє їх. Дозволити безповоротне видалення повідомлень, тільки якщо ваш контакт дозволяє вам. (24 години) Дозволити голосові повідомлення\? Пароль застосунку замінено паролем самознищення. @@ -29,41 +29,41 @@ Сховище ключів Android використовується для безпечного збереження ключової фрази - це дозволяє службі сповіщень працювати. Адміністратори можуть створювати посилання для приєднання до групи. Збірка додатку: %s - Дозволяйте голосові повідомлення тільки в разі дозволу вашого контакту. - Дозволяйте надсилати повідомлення, які зникають. + Дозволити голосові повідомлення тільки за умови, що ваш контакт дозволяє їх. + Дозволити надсилати зникаючі повідомлення. прийнятий виклик Завжди використовувати реле ДОДАТОК - Дозволяйте надсилати прямі повідомлення учасникам. + Дозволити надсилання приватних повідомлень учасникам. Дозволити безповоротно видаляти надіслані повідомлення. (24 години) Дозволяйте надсилати голосові повідомлення. Дозволити реакції на повідомлення. Вся інформація стирається при його введенні. Пароль для додатка ІКОНКА ДОДАТКУ - Дозволяйте повідомлення, які зникають, тільки якщо ваш контакт дозволяє їх. + Дозволити зникаючі повідомлення тільки за умови, що ваш контакт дозволяє їх. Дозвольте вашим контактам додавати реакції на повідомлення. - Дозволяйте реакції на повідомлення тільки в разі дозволу вашого контакту. + Дозволити реакції на повідомлення тільки за умови, що ваш контакт дозволяє їх. Створений порожній профіль чату з наданим ім\'ям, і застосунок відкривається, як завжди. Додатковий акцент Дозволити вашим контактам безповоротно видаляти надіслані повідомлення. (24 години) Дозволити Розширені налаштування мережі Отримувати доступ до серверів через SOCKS-проксі на порті %d? Проксі має бути запущено до активації цієї опції. - Всі ваші контакти залишаться підключеними. - Всі дані застосунку буде видалено. + Усі ваші контакти залишаться підключеними. + Усі дані застосунку буде видалено. Після перезапуску додатка або зміни ключової фрази буде використано сховище ключів Android для безпечного збереження ключової фрази - це дозволить отримувати сповіщення. Дозвольте вашим контактам надсилати голосові повідомлення. Прийняти інкогніто - Додати сервер… + Додати сервер адміністратор Додати привітання - Всі члени групи залишаться підключеними. + Усі учасники групи залишаться підключеними. Дозвольте вашим контактам надсилати повідомлення, які зникають. - Всі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас. + Усі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас. Версія додатку Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Оновлення профілю буде відправлено вашим контактам. - Всі ваші контакти залишаться підключеними. Оновлення профілю буде відправлено вашим контактам. + Усі ваші контакти залишаться підключеними. Оновлення профілю буде відправлено вашим контактам. Відповісти на виклик Адреса Додати профіль @@ -75,12 +75,11 @@ для кожного профілю чату, який у вас є в додатку.]]> Вигляд Версія додатку: v%s - Окреме TCP-підключення (і обліковий запис SOCKS) буде використовуватися для кожного контакту та учасника групи. -\nЗверніть увагу: якщо у вас багато підключень, споживання заряду батареї та трафіку може значно збільшитися, і деякі підключення можуть бути невдалими. + для кожного контакту та члена групи.\nЗверніть увагу: якщо у вас багато з’єднань, заряд акумулятора та споживання трафіку можуть бути значно вищими, а деякі з’єднання можуть бути невдалими.]]> Активована оптимізація батареї, вимикається фоновий сервіс і періодичні запити нових повідомлень. Ви можете знову увімкнути їх у налаштуваннях. Назад жирний - Добре для акумулятора. Сервіс фонового запуску перевіряє повідомлення кожні 10 хвилин. Ви можете пропустити виклики чи важливі повідомлення.]]> + Добре для акумулятора. Додаток перевіряє повідомлення кожні 10 хвилин. Ви можете пропустити виклики чи важливі повідомлення.]]> Аудіо та відеовиклики Аудіо вимкнено Аутентифікація недоступна @@ -104,7 +103,7 @@ поганий хеш повідомлення поганий ідентифікатор повідомлення Фон - Це можна вимкнути у налаштуваннях – сповіщення все одно будуть відображатися, коли програма працює. + Це можна вимкнути в налаштуваннях – сповіщення все одно відображатимуться під час роботи програми.]]> Служба фонового режиму завжди активна – сповіщення відображатимуться, як тільки повідомлення будуть доступні. Запит на отримання зображення Прикріпити @@ -220,21 +219,20 @@ OK Скопійовано в буфер обміну Для підключення через посилання - Відкрити у мобільному додатку, а потім торкніться Підключити в додатку.]]> + Відкрити у мобільному додатку, а потім торкніться Підключити в додатку.]]> Приглушити Скасувати приглушення Ви запросили контакт Контакт, якому ви поділилися посиланням, НЕ зможе підключитися! - заповнювач зображення профілю + аватар не встановлено QR-код довідка покажіть QR-код у відеовиклику, або поділіться посиланням.]]> - Ваш профіль чату буде відправлено -\nвашому контакту + Ваш профіль буде відправлено \nвашому контакту Одноразове запрошення Невірний код безпеки! Для перевірки end-to-end шифрування порівняйте (або скануйте) код на своїх пристроях. - Ваші налаштування + Налаштування Ваша SimpleX-адреса Допомога з Markdown Блокування SimpleX @@ -244,14 +242,13 @@ Використовувати для нових підключень Видалити сервер Оцінити на GitHub - Як використовувати ваші сервери + Як використовувати власні сервери Збережені сервери WebRTC ICE будуть видалені. Налаштувати сервери ICE Мережа та сервери - Налаштування мережі + Розширені налаштування Використовувати SOCKS-проксі? Використовувати прямий підключення до Інтернету? - Оновити налаштування .onion-хостів? Використовувати .onion-хости Якщо доступно Ні @@ -267,13 +264,13 @@ Введіть своє ім\'я: дзвінок в процесі запуск… - Перша платформа без ідентифікаторів користувачів – приватна за конструкцією. + Ніяких ідентифікаторів користувачів. Децентралізована Використовувати чат - Це можна змінити пізніше в налаштуваннях. + Як це впливає на батарею Миттєво Виклик вже завершено! - Ваші виклики + Виклики Ваші сервери ICE Відкрити через реле @@ -295,8 +292,6 @@ Зашифрувати базу даних\? Неправильна ключова фраза! Введіть правильну ключову фразу. - Архів чату - АРХІВ ЧАТУ підключив(лась) змінив(ла) вашу роль на %s ви змінили свою роль на %s @@ -376,12 +371,11 @@ Виклики на екрані блокування: від абонента до абонента Завершити дзвінок - Створено %1$s Розгорнути вибір ролі так Налаштування контакту Безпека SimpleX Chat була перевірена компанією Trail of Bits. - Ваші контакти можуть дозволити повне видалення повідомлень. + Ваші контакти можуть дозволити остаточне видалення повідомлень. База даних буде зашифрована. Помилка сховища ключів Невідома помилка @@ -395,7 +389,7 @@ Стан мережі Оновити налаштування мережі\? Локальні дані профілю тільки - Ваш випадковий профіль + Випадковий профіль ввімкнено ввімкнено для вас Забороняйте надсилання прямих повідомлень учасникам. @@ -412,9 +406,8 @@ Ім\'я профілю: очікування підтвердження… Приватність перевизначена - Люди можуть підключатися до вас лише за допомогою посилань, які ви надаєте. + Ви вирішуєте, хто може під\'єднатися. Як працює SimpleX - Докладніше читайте в нашому репозиторії на GitHub. зашифрований e2e аудіовиклик Відкрийте SimpleX Chat для прийняття виклику e2e зашифровано @@ -423,7 +416,7 @@ Пропущений виклик Підключення виклику Конфіденційність і безпека - Ваша конфіденційність + Конфіденційність НАЛАШТУВАННЯ ДОПОМОГА ПІДТРИМАЙТЕ SIMPLEX CHAT @@ -449,8 +442,7 @@ Створити посилання на групу Змінити роль у групі\? Помилка при вилученні учасника - Ваш профіль чату буде відправлений учасникам групи - Зберегти колір + Ваш профіль буде відправлений учасникам групи Видалення для всіх Голосові повідомлення Голосові повідомлення заборонені в цьому чаті. @@ -458,20 +450,20 @@ Встановіть його замість системної аутентифікації. Вимкнути\? Поділитися з контактами - Ваш профіль зберігається на вашому пристрої і обмінюється лише з ваших контактів. Сервери SimpleX не можуть його бачити. - Зберегти і повідомити контакти - Зберегти і повідомити учасників групи + Ваш профіль зберігається на вашому пристрої та ділиться лише з вашими контактами. Серверам SimpleX профіль недоступний. + Зберегти та сповістити контакти + Зберегти та сповістити учасників Вийти без збереження Сховати профіль Пароль для відображення Створити без зашифрування e2e контакт має зашифрування e2e - Хеш попереднього повідомлення відрізняється. + Хеш попереднього повідомлення інший.\" Підтвердити пароль Новий пароль Перезапустити - Ваша база даних чату + База даних чату Чат зупинено БАЗА ДАНИХ ЧАТУ Новий архів бази даних @@ -503,8 +495,8 @@ Тільки ваш контакт може надсилати голосові повідомлення. Забороняйте надсилання повідомлень, які зникають. Забороняйте невідворотне видалення повідомлень. - Учасники групи можуть надсилати голосові повідомлення. - %dm + Учасники можуть надсилати голосові повідомлення. + %dм Нове в %s Самознищуючий пароль Італійський інтерфейс @@ -523,21 +515,20 @@ Зберігайте останню чернетку повідомлення із вкладеннями. Зникне повідомлення Надіслати зникне повідомлення - зображення профілю + аватар Більше Створити профіль GitHub.]]> Відео увімкнено Це може трапитися, якщо ви або ваше з\'єднання використовували застарілу резервну копію бази даних. Відновити резервну копію бази даних - Зберегти архів запрошення до групи %1$s Вас запрошено в групу. Приєднуйтесь, щоб спілкуватися з учасниками групи. Реакції на повідомлення Встановити на 1 день Забороняйте реакції на повідомлення. Реакції на повідомлення заборонені в цьому чаті. - %ds + %dс хвилини Китайський та іспанський інтерфейс підключення %1$d @@ -552,7 +543,7 @@ Створити файл Помилка видалення користувача Помилка оновлення конфіденційності користувача - фоновий сервіс SimpleX – він використовує кілька відсотків батареї щодня.]]> + SimpleX працює у фоновому режимі замість використання пуш-повідомлень.]]> Періодичні сповіщення Служба чату SimpleX Перевіряє нові повідомлення кожні 10 хвилин протягом 1 хвилини @@ -562,7 +553,7 @@ Блокування SimpleX Щоб захистити вашу інформацію, увімкніть блокування SimpleX. \nВам буде запропоновано завершити аутентифікацію перед увімкненням цієї функції. - Увійти за допомогою своїх облікових даних + Пройдіть аутентифікацію Увімкнути блокування SimpleX Блокування SimpleX не увімкнено! Поділитися @@ -577,8 +568,8 @@ Файл буде видалено з серверів. Відкликати несанкціонована відправка - Ласкаво просимо, %1$s! - Ласкаво просимо! + Вітаємо, %1$s! + Вітаємо! Цей текст доступний у налаштуваннях вас запрошено в групу Поділитися повідомленням… @@ -588,7 +579,7 @@ Одночасно можна відправити лише 10 відео Помилка декодування зображення Неможливо декодувати зображення. Спробуйте інше зображення або зв\'яжіться з розробниками. - Зображення + Фото Зображення буде отримано, коли ваш контакт завершить його вивантаження. Зображення буде отримано, коли ваш контакт буде в мережі, будь ласка, зачекайте або перевірте пізніше! Зображення збережено в галереї @@ -617,8 +608,8 @@ Логотип SimpleX Електронна пошта Цей QR-код не є посиланням! - Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, зачекайте або перевірте пізніше! - Вас підключать, коли ваш запит на з\'єднання буде прийнятий, зачекайте або перевірте пізніше! + Ви будете підключені до групи, коли пристрій власник групи буде в мережі, зачекайте або перевірте пізніше! + Підключення відбудеться, коли ваш запит на підключення буде прийнято. Будь ласка, зачекайте або спробуйте пізніше! Поділитися 1-разовим посиланням Дізнатися більше Щоб підключитися, ваш контакт може сканувати QR-код або використовувати посилання у додатку. @@ -628,29 +619,27 @@ Цей рядок не є з\'єднувальним посиланням! Код безпеки Позначити, що перевірено - Ваші профілі чату + Ваші профілі Пароль бази даних та експорт Markdown у повідомленнях Надсилайте питання та ідеї - Ввести адресу сервера вручну + Ввести сервер вручну Попередньо встановлений сервер Адреса вашого сервера - Сервери для нових підключень вашого поточного профілю чату + Сервери для нових підключень до вашого поточного профілю Використовувати сервери SimpleX Chat? Сервери ICE (один на рядок) Помилка збереження серверів ICE .Onion-хости будуть обов\'язковими для підключення. \nЗверніть увагу: ви не зможете підключитися до серверів без адреси .onion. - Хости .onion будуть використовуватися, якщо доступні. - Хости .onion будуть обов\'язковими для підключення. Показати параметри розробника Ідентифікатори бази даних та опція ізоляції транспорту. Сповіщення перестануть працювати, поки ви не перезапустите додаток Ви можете створити його пізніше Ваш поточний профіль Видалити зображення - Зберегти уподобання? - Зберегти і повідомити контакт + Зберегти налаштування? + Зберегти та сповістити контакт Зберегти пароль профілю Пароль схованого профілю Профіль обмінюється лише з вашими контактами. @@ -659,11 +648,9 @@ Ви можете використовувати markdown для форматування повідомлень: Створіть свій профіль Створіть приватне підключення - як в SimpleX можливо доставляти повідомлення, якщо він не має ідентифікаторів користувачів?]]> - отримувати повідомлення, ваші контакти – сервери, які ви використовуєте для надсилання повідомлень їм.]]> - шифрування на двох рівнях.]]> + Тільки клієнтські пристрої зберігають профілі, контакти, групи та повідомлення. Приватні сповіщення - Споживає більше заряду акумулятора! Фоновий сервіс завжди працює – сповіщення відображаються, як тільки повідомлення доступні.]]> + Споживає більше акумулятора! Додаток завжди працює у фоновому режимі – сповіщення відображаються миттєво.]]> Вставте отримане посилання Відео вимкнено Завершено виклик @@ -730,10 +717,10 @@ Зникає о: %s (поточне) Вилучити учасника - Роль буде змінено на \"%s\". Всі учасники групи будуть сповіщені. - Роль буде змінено на \"%s\". Учасник отримає нове запрошення. + Роль буде змінено на %s. Усі учасники групи будуть сповіщені. + Роль буде змінено на "%s". Учасник отримає нове запрошення. Група - Ласкаво просимо + Привітальне повідомлення Профіль групи зберігається на пристроях учасників, а не на серверах. Зберегти профіль групи Профіль і підключення до серверів @@ -742,10 +729,10 @@ Показати профіль Показати профіль чату Коли ви ділитесь анонімним профілем з кимось, цей профіль буде використовуватися для груп, до яких вас запрошують. - Світла + Світлий Помилка імпорту теми Налаштування групи - Повідомлення зникнення + Зникаючі повідомлення ввімкнено для контакту вимкнено отримано, заборонено @@ -758,18 +745,18 @@ Тільки ви можете надсилати голосові повідомлення. Тільки ви можете додавати реакції на повідомлення. Заборонити реакції на повідомлення. - Самознищувальні повідомлення заборонені в цій групі. - Учасники групи можуть надсилати приватні повідомлення. - Приватні повідомлення між учасниками заборонені в цій групі. - Учасники групи можуть назавжди видаляти відправлені повідомлення. (24 години) - Назавжди видалення повідомлень заборонене в цій групі. - Голосові повідомлення заборонені в цій групі. - Учасники групи можуть додавати реакції на повідомлення. - Реакції на повідомлення заборонені в цій групі. + Повідомлення, що зникають, заборонені. + Учасники можуть надсилати прямі повідомлення. + Приватні повідомлення між учасниками заборонені в цій групі. + Учасники можуть необоротно видаляти надіслані повідомлення (протягом 24 годин). + Заборонено необоротне видалення повідомлень. + Голосові повідомлення заборонені + Учасники можуть додавати реакції на повідомлення. + Реакції на повідомлення заборонені. %d година %d тиждень %d тижні - %dw + %dтиж запропоновано %s: %2s З опційним вітанням. Приховуйте екран додатка в останніх програмах. @@ -780,7 +767,7 @@ Дякуємо користувачам – приєднуйтеся через Weblate! Тепер адміністратори можуть: \n- видаляти повідомлення учасників. -\n- вимикати учасників (роль спостерігач) +\n- вимикати учасників (роль спостерігача). Встановіть повідомлення, яке показується новим учасникам! Додатково зменшено використання батареї Більше поліпшень незабаром! @@ -797,7 +784,7 @@ Ви вже підключені до %1$s через це посилання. Режим інкогніто СЕРВЕРИ - Зберегти ласкаво просимо? + Зберегти вітальне повідомлення? Отримання через Приглушено, коли неактивно! Видалити профіль @@ -817,7 +804,7 @@ Безпечна черга Видалити чергу Будь ласка, перевірте, що ви використали правильне посилання або попросіть вашого контакту вислати інше. - дозвольте SimpleX працювати в фоновому режимі в наступному діалозі. В іншому випадку сповіщення будуть вимкнені.]]> + Дозвольте це в наступному діалозі, щоб отримувати сповіщення миттєво.]]> Миттєві сповіщення Контакт прихований: нове повідомлення @@ -839,11 +826,11 @@ Інший час Створити одноразове запрошення Сканувати QR-код - Зображення + Фото Відео Прийняте вами з\'єднання буде скасоване! Контакт ще не підключений! - Тестові сервери + Тестувати сервери Зберегти сервери Ваш сервер Тест сервера не вдався! @@ -889,7 +876,7 @@ Надіслано о: %s Видалено о: %s %s (поточне) - %dh + %dч %d день %d днів скасовано %s @@ -904,14 +891,13 @@ Дякуємо користувачам – приєднуйтеся через Weblate! Режим блокування SimpleX Системна аутентифікація - Для захисту приватності, замість ідентифікаторів користувачів, які використовуються всіма іншими платформами, у SimpleX є ідентифікатори черг повідомлень, окремі для кожного з ваших контактів. + Для захисту вашої конфіденційності SimpleX використовує окремі ID для кожного вашого контакту. Коли додаток запущено Періодично контакт не має зашифрування e2e Увімкнути блокування Пароль не змінено! Змінити режим блокування - Зупиніть чат, щоб увімкнути дії з базою даних. Перезапустіть додаток, щоб створити новий профіль чату. Видалити файли для всіх профілів чату Видалити файли та медіа? @@ -932,7 +918,7 @@ ви видалили %1$s Торкніться для активації профілю. Забороняйте надсилання голосових повідомлень. - Учасники групи можуть надсилати самознищувальні повідомлення. + Учасники можуть надсилати повідомлення, що зникають. %d хв Зменшене споживання енергії батареї Редагувати зображення @@ -951,10 +937,9 @@ Тільки власники груп можуть увімкнути голосові повідомлення. Відхилити Очистити чат\? - зображення попереднього перегляду посилання + зображення прев’ю посилання скасувати попередній перегляд посилання Налаштування - Хости .onion не будуть використовуватися. Виклик у процесі Помилка бази даних Відновити @@ -998,16 +983,15 @@ ДЛЯ КОНСОЛІ Учасника буде вилучено з групи - цю дію неможливо скасувати! Змінити роль - Відновити Ви все ще отримуватимете дзвінки та сповіщення від приглушених профілів, коли вони активні. %d місяці - %dmth + %dміс Надіслані повідомлення будуть видалені після встановленого часу. Відкриття бази даних… Помилка встановлення адреси Підключити Будь ласка, запам\'ятайте або збережіть його надійно - немає можливості відновлення втраченого пароля! - Відкрити профілі чату + Змінити профілі чату Очікування на відео Очікування на файл Голосове повідомлення (%1$s) @@ -1019,15 +1003,15 @@ Підключитися за посиланням / QR-кодом Очистити Неправильний QR-код - Вас підключать, коли пристрій вашого контакту буде в мережі, зачекайте або перевірте пізніше! + Підключення відбудеться, коли пристрій вашого контакту буде онлайн. Будь ласка, зачекайте або спробуйте пізніше! Ви не втратите свої контакти, якщо ви пізніше видалите свою адресу. Коли люди просять про з\'єднання, ви можете його прийняти чи відхилити. Посібнику користувача.]]> SimpleX-адреса - Очистити перевірку + Скинути підтвердження %s перевірено %s не перевірено - Надішліть нам електронного листа + Написати нам ел. листа Тестовий сервер Зберегти сервери\? Ваші сервери ICE @@ -1038,9 +1022,9 @@ Хост Порт Обов\'язково - КОЛОРИ ТЕМИ + КОЛЬОРИ ІНТЕРФЕЙСУ Створіть адресу, щоб дозволити людям підключатися до вас. - Ваші контакти залишаться підключеними. + Контакти залишатимуться підключеними. Створити SimpleX-адресу Оновлення профілю буде відправлено вашим контактам. Зупинити поділ адреси? @@ -1059,7 +1043,7 @@ підключення… підключено завершено - Стійка до спаму та зловживань + Стійкий до спаму %1$s хоче підключитися до вас через зашифрований e2e відеовиклик Ігнорувати @@ -1076,8 +1060,6 @@ Для відкриття чату потрібна ключова фраза бази даних. Приєднатися до групи\? Оновлення бази даних - Видалити архів - Видалити архів чату\? Вийти з групи? Групу не знайдено! Ця група більше не існує. @@ -1086,12 +1068,12 @@ Привітання Вибрати контакти Поділитися адресою - Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите членів групи, якщо потім видалите її. + Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо потім видалите її. Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли підключитися до %s. Локальна назва Ідентифікатор бази даних Попередній перегляд - Введіть ласкаво просимо… + Введіть привітальне повідомлення… Змінити адресу отримання Створити секретну групу Повністю децентралізовано - видимо тільки для учасників. @@ -1105,8 +1087,8 @@ Ви дозволяєте ні вимк - Встановити налаштування групи - Ваші налаштування + Налаштування групи + Налаштування Прямі повідомлення Помилка Одноразове запрошення @@ -1126,7 +1108,7 @@ Видалити контакт Встановити ім\'я контакту… Файл - З галереї + Галерея Команда SimpleX хоче підключитися до вас! ЕКСПЕРИМЕНТАЛЬНІ ФУНКЦІЇ @@ -1149,9 +1131,9 @@ Файл не знайдено Підключитися через посилання Підключитися - Щоб показати ваш схований профіль, введіть повний пароль у поле пошуку на сторінці Ваші профілі чату. + Щоб показати ваш схований профіль, введіть повний пароль у поле пошуку на сторінці Ваші профілі. Підтвердити пароль - %dd + %dд Захистіть свої чат-профілі паролем! Помилка дешифрування Сервер вимагає авторизації для завантаження, перевірте пароль @@ -1163,7 +1145,7 @@ Дзвінки чату SimpleX Служба сповіщень Показати попередній перегляд - Попередній перегляд сповіщень + Перегляд сповіщень Запускається, коли додаток відкритий Запускається періодично Текст повідомлення @@ -1178,8 +1160,8 @@ кольоровий дзвінок завершено %1$s помилка дзвінка - Наступне покоління приватного обміну повідомленнями - Відкритий протокол та код – кожен може запустити сервери. + Майбутнє обміну повідомленнями + Кожен може хостити сервери. Інструменти розробника Експериментальні функції ДЗВІНКИ @@ -1195,7 +1177,7 @@ (щоб поділитися з вашим контактом) (сканувати або вставити з буферу обміну) підключитися до розробників SimpleX Chat, щоб задати будь-які питання і отримувати оновлення.]]> - Сканувати QR-код.]]> + Сканувати QR-код.]]> Адреса SimpleX Показати QR-код Приєднання до групи @@ -1217,16 +1199,14 @@ Експортувати тему Переконайтеся, що файл має правильний синтаксис YAML. Експортуйте тему, щоб мати приклад структури файлу теми. Скинути кольори - за замовчуванням (%s) + типово (%s) Тільки ви можете здійснювати дзвінки. Тільки ваш контакт може здійснювати дзвінки. Що нового Голосові повідомлення За профілем чату (типово) або за підключенням (BETA). Власні теми - - голосові повідомлення до 5 хвилин. -\n- власний час на зникнення. -\n- історія редагування. + - голосові повідомлення до 5 хвилин.\n- налаштування часу для зникнення повідомлень.\n- історія змін. Японський та португальський інтерфейс Натисніть, щоб приєднатися Натисніть, щоб приєднатися анонімно @@ -1240,13 +1220,13 @@ Використовувати .onion-хости на Ні, якщо SOCKS-проксі їх не підтримує.]]> Показати: Сховати: - Під час імпорту сталися деякі невідновні помилки - ви можете переглянути консоль чату для отримання більше деталей. + Під час імпорту відбулися деякі непередбачувані помилки: Цю дію неможливо відмінити - будуть видалені повідомлення, відправлені та отримані раніше вибраного часу. Це може зайняти декілька хвилин. Вам потрібно вводити ключову фразу кожен раз при запуску додатка - вона не зберігається на пристрої. Змінити ключову фразу бази даних? Не вдається отримати доступ до сховища ключів для збереження пароля бази даних Зберегти ключову фразу і відкрити чат - Ключова фраза не знайдена в сховищі ключів, будь ласка, введіть її вручну. Це може трапитися, якщо ви відновили дані додатка за допомогою інструменту резервного копіювання. Якщо це не так, зверніться до розробників. + Пароль не знайдено в сховищі ключів, введіть його вручну. Це могло статися, якщо ви відновили дані додатка за допомогою інструмента резервного копіювання. Якщо це не так, зверніться до розробників. Видалити профіль чату %d сек Пошук @@ -1266,7 +1246,7 @@ Помилка відміни зміни адреси Перервати зміну адреси Дозволити надсилання файлів та медіафайлів. - Файли та медіафайли заборонені в цій групі. + Файли та медіа заборонені. Підключити інкогніто Використовувати поточний профіль Дозволити @@ -1364,7 +1344,7 @@ Зміна адреси буде скасована. Буде використовуватися стара адреса для отримання. Повторно узгодити шифрування? Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок підключення! - Учасники групи можуть надсилати файли та медіафайли. + Учасники можуть надсилати файли та медіа. База даних буде зашифрована, і ключова фраза буде збережена в налаштуваннях. Розгорнути Повторити запит на підключення? @@ -1438,7 +1418,7 @@ %d подій в групі Невірне ім\'я! Перемикайте інкогніто під час підключення. - Це ваше посилання для групи %1$s! + %1$s!]]> Розблокувати Неправильний шлях до файлу - підключайтесь до служби каталогів (BETA)! @@ -1448,7 +1428,7 @@ Ви вже подали запит на підключення за цією адресою! надіслати приватне повідомлення Показувати консоль в новому вікні - Всі нові повідомлення від %s будуть приховані! + Усі нові повідомлення від %s будуть приховані! підключив(лась) безпосередньо заблоковано Блокувати учасників групи @@ -1456,35 +1436,35 @@ Підключений робочий стіл Новий мобільний пристрій Підключати автоматично - Адреса робочого столу + Адреса комп\'ютера Одночасно може працювати лише один пристрій - Посилання на мобільний та комп\'ютерний додатки! 🔗 + Підключіть мобільний і десктопний додатки! 🔗 Через безпечний квантовостійкий протокол. - Використовувати з робочого столу у мобільному додатку і скануйте QR-код.]]> + Використовувати з комп\'ютера в мобільному додатку та відскануйте QR-код.]]> Щоб приховати небажані повідомлення. Несумісна версія (новий)]]> - Відсунути відсилання до робочого столу? + Відключити комп\'ютер? Кращі групи - Параметри пов\'язаних робочих столів - Пов\'язані робочі столи + Параметри пов\'язаних комп\'ютерів + Підключені комп\'ютери Виявити через локальну мережу Інкогніто групи Цей пристрій %s був відключений]]> - Очікування робочого столу… + Очікування комп\'ютера… Швидше приєднуйтесь та надійшовні повідомлення. - Пов\'язані мобільні + Під’єднані мобільні Робочий стіл - Підключено до робочого столу + Підключено до комп\'ютера Назва цього пристрою Завантаження файлу - Підключення до робочого столу + Підключення до комп\'ютера Знайдено робочий стіл - Пристрої робочого столу + Комп\'ютери Не сумісно! Зв\'язати з мобільним - Використовувати зі стаціонарного комп\'ютера + Використовувати з комп\'ютера Підключений мобільний Код сеансу Підключення завершено @@ -1494,14 +1474,14 @@ Перевірте код на мобільному Введіть назву цього пристрою… Помилка - Підключитися до робочого столу + Підключитися до комп\'ютера Відключити автор Підключено до мобільного - Некоректна адреса робочого столу - Вставити адресу робочого столу - Перевірити код з робочим столом - Сканувати QR-код з робочого столу + Некоректна адреса комп\'ютера + Вставити адресу комп\'ютера + Перевірити код з комп\'ютером + Сканувати QR-код з комп\'ютера Пристрої Виявлено через локальну мережу - за бажанням повідомляйте про видалених контактів. @@ -1511,7 +1491,7 @@ Перевірити підключення Відключити робочий стіл? Будь ласка, зачекайте, поки файл завантажується з підключеного мобільного - Версія робочого столу %s не сумісна з цим додатком. + Версія комп\'ютерного додатка %s несумісна з цим додатком. Перевірити підключення Чат зупинено. Якщо ви вже використовували цю базу даних на іншому пристрої, перенесіть її назад перед запуском чату. З зашифрованими файлами та медіа. @@ -1573,7 +1553,7 @@ \n \nРекомендується перезапустити додаток. Надсилати до 100 останніх повідомлень новим користувачам. - До 100 останніх повідомлень надсилаються новим членам. + До 100 останніх повідомлень надсилаються новим учасникам. З\'єднання перервано Створено: %s Показати внутрішні помилки @@ -1628,7 +1608,7 @@ учасник %1$s змінений на %2$s вилучено зображення профілю Установлено нову адресу контакту - Установлено нове зображення профілю + встановити новий аватар оновлений профіль вилучено адресу контакту Колишній учасник %1$s @@ -1637,7 +1617,7 @@ Аудіодзвінок Помилка відкриття браузера Для використання дзвінків потрібен браузер за замовчуванням. Будь ласка, налаштуйте браузер за замовчуванням в системі та надайте більше інформації розробникам. - Перехід з іншого пристрою + Перенести з іншого пристрою квантово-стійке шифрування e2e стандартне наскрізне шифрування Цей чат захищений наскрізним шифруванням. @@ -1664,9 +1644,9 @@ Імпорт архіву Повторний імпорт Завершіть міграцію на іншому пристрої. - Подати заявку - Перенести пристрій - Перехід на інший пристрій + Застосувати + Міграція пристрою + Перенести на інший пристрій Помилка експорту бази даних чату Налаштування збереження помилок Помилка завантаження архіву @@ -1677,7 +1657,7 @@ Помилка видалення бази даних Для того, щоб продовжити, чат слід зупинити. Зупинка чату - %s завантажено + %s вивантажено Завантаження архіву Створення архівного посилання Видалити базу даних з цього пристрою @@ -1694,7 +1674,7 @@ Перевірте підключення до Інтернету та спробуйте ще раз Переконайтеся, що ви пам\'ятаєте пароль до бази даних для її перенесення. Помилка при перевірці парольної фрази: - Всі ваші контакти, розмови та файли будуть надійно зашифровані та завантажені частинами на налаштовані XFTP-реле. + Усі ваші контакти, розмови та файли будуть надійно зашифровані та завантажені частинами на налаштовані XFTP-реле. Please note: використання однієї і тієї ж бази даних на двох пристроях порушить розшифровку повідомлень з ваших з\'єднань, як захист безпеки.]]> Скасувати міграцію Чат перемістився! @@ -1713,4 +1693,699 @@ Використовуйте додаток під час розмови. Підтвердіть парольну фразу Ви можете спробувати ще раз. - \ No newline at end of file + Перевищено ліміт - одержувач не отримав раніше надіслані повідомлення. + Помилка сервера призначення: %1$s + Помилка: %1$s + Попередження про доставку повідомлення + Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його. + Сервер переадресації: %1$s +\nПомилка сервера призначення: %2$s + Сервер переадресації: %1$s +\nПомилка: %2$s + Мікрофон + Джерело повідомлення залишається приватним. + Завжди + Завжди використовуйте приватну маршрутизацію. + Режим маршрутизації повідомлень + НЕ використовуйте приватну маршрутизацію. + Ні + НЕ надсилайте повідомлення напряму, навіть якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + Камера + Надати в налаштуваннях + Навушники + Стільниковий + власники + Більш надійне з\'єднання з мережею. + Керування мережею + Пересилання та збереження повідомлень + Переадресувати повідомлення… + Дозволити зниження рейтингу + Тема програми + Темний режим + Завантажити + Навушник + Увімкнено для + Повідомлення про помилку, зв\'яжіться з розробниками. + Файли та медіафайли заборонені + Знайдіть цей дозвіл у налаштуваннях Android і надайте його вручну. + Переслати + Переслано + Переслано з + Учасники можуть надсилати посилання SimpleX. + Звуки вхідного дзвінка + Світлий режим + Запасний варіант маршрутизації повідомлень + МАРШРУТИЗАЦІЯ ПРИВАТНИХ ПОВІДОМЛЕНЬ + переслано + Інше + Дозволити надсилати посилання SimpleX. + Заборонити надсилання посилань SimpleX + Немає підключення до мережі + Ніколи + Приватна маршрутизація + Bluetooth + Камера та мікрофон + Надайте дозвіл(и) на здійснення дзвінків + Відкрити налаштування + ФАЙЛИ + Зображення профілів + Підключення до мережі + адміністратори + всіх учасників + Литовський інтерфейс + Надавати дозволи + Кольори чату + Тема чату + Тема профілю + Темна + Додатковий акцент 2 + Розширені налаштування + Усі кольорові режими + Застосувати до + Колірний режим + Темна + Кольори темного режиму + Заповнити + Підходить + Доброго дня! + Доброго ранку! + Світлий + Видалити зображення + Помилка ініціалізації WebView. Оновіть систему до нової версії. Зверніться до розробників. +\nПомилка: %s + Підтвердити файли з невідомих серверів. + Покращена доставка повідомлень + Перський інтерфейс + Маршрутизація приватних повідомлень 🚀 + Захист IP-адреси + Захистіть свою IP-адресу від ретрансляторів повідомлень, обраних вашими контактами. +\nУвімкніть у налаштуваннях *Мережа та сервери*. + Отримано відповідь + Збережено + Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion або коли ввімкнено SOCKS-проксі). + Надсилайте повідомлення напряму, якщо IP-адреса захищена, а ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + Надсилайте повідомлення напряму, якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + Встановлення теми за замовчуванням + Надіслано відповідь + Показати список чату в новому вікні + Використовуйте приватну маршрутизацію з невідомими серверами. + Використовуйте приватну маршрутизацію з невідомими серверами, якщо IP-адреса не захищена. + Фон шпалер + Акцент на шпалерах + Повторити + Масштаб + Нехай ваші чати виглядають інакше! + Нові теми чату + Безпечне отримання файлів + З меншим споживанням заряду акумулятора. + Неправильний ключ або невідоме з\'єднання - швидше за все, це з\'єднання видалено. + Адреса сервера несумісна з налаштуваннями мережі. + Голосові повідомлення заборонені + WiFi + Спікер + Повернутися до теми програми + Повернутися до теми користувача + Посилання SimpleX + Квадрат, коло або щось середнє між ними. + Буде ввімкнено в прямих чатах! + збережено + збережено з %s + Дротова мережа Ethernet + Невідомі сервери! + Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів: +\n%1$s. + Одержувач(и) не бачить, від кого це повідомлення. + Збережено з + Серверна версія несумісна з мережевими налаштуваннями. + Посилання SimpleX заборонені + Показати статус повідомлення + Щоб захистити вашу IP-адресу, приватна маршрутизація використовує ваші SMP-сервери для доставки повідомлень. + Невідомі сервери + Незахищений + Коли IP приховано + Так + Отримання паралелізму + Посилання SimpleX заборонені. + Форма зображень профілю + При підключенні аудіо та відеодзвінків. + Скинути колір + Система + Без Tor або VPN ваша IP-адреса буде видимою для файлових серверів. + немає + Доставка налагодження + Інформація про чергу повідомлень + інформація про чергу на сервері: %1$s +\n +\nостаннє отримане повідомлення: %2$s + Адреса сервера призначення %1$s несумісна з налаштуваннями сервера переадресації %2$s. + Файл не знайдено — ймовірно, файл був видалений або скасований. + Сканувати / Вставити посилання + Налаштовані XFTP сервери + Бета + Статус файлу + Надіслано повідомлень + Статистика + Попередньо підключені сервери + Помилковий ключ або невідома адреса чанка файлу - найбільш імовірно, що файл було видалено. + Недійсне посилання + Будь ласка, перевірте, чи правильне посилання SimpleX. + Адреса сервера несумісна з налаштуваннями мережі: %1$s. + Помилка підключення до сервера переадресації %1$s. Будь ласка, спробуйте пізніше. + Адреса сервера переадресації несумісна з налаштуваннями мережі: %1$s. + Версія сервера переадресації несумісна з налаштуваннями мережі: %1$s. + Помилка приватного маршрутизації + Версія сервера призначення %1$s несумісна з сервером переадресації %2$s. + Будь ласка, спробуйте пізніше. + Помилка сервера файлів: %1$s + Учасник неактивний + Повідомлення переслано + Поки що немає прямого з\'єднання, повідомлення пересилається адміністратором. + Вибрано %d + Повідомлення + Помилка файлу + Тимчасова помилка файлу + Контакт буде видалено - це неможливо скасувати! + Видалити лише розмову + Архівовані контакти + Нагадати пізніше + Стабільна + Новий досвід чату 🎉 + Адреса сервера + Підтвердити видалення контакту? + підключитися + Сервер переадресації %1$s не зміг з\'єднатися з цільовим сервером %2$s. Будь ласка, спробуйте пізніше. + Повідомлення може бути доставлено пізніше, якщо учасник стане активним. + Нічого не вибрано + Відкрити налаштування сервера + Версія сервера несумісна з вашим додатком: %1$s. + Повідомлення будуть позначені як модеровані для всіх учасників. + Оновлювати додаток автоматично + Не вдалося надіслати повідомлення + Вибрані налаштування чату забороняють це повідомлення. + Статус файлу: %s + Статус повідомлення: %s + Нове повідомлення + Створити + Запросити + Перемикнути список чатів: + Ви можете змінити це в налаштуваннях зовнішнього вигляду. + Статус повідомлення + Архівувати контакти, щоб поговорити пізніше. + Відтворити зі списку чатів. + Нові опції медіа + Розмиття для кращої конфіденційності. + Збільшити розмір шрифту. + Він захищає вашу IP-адресу та з\'єднання. + Перевірити, чи є оновлення + Завантажуйте нові версії з GitHub. + Дозволити дзвінки? + Контакт видалено. + TCP з\'єднання + Масштабування + Розмір шрифту + Будь ласка, попросіть вашого контакту увімкнути дзвінки. + Зберегти і перепідключитися + Видалити до 20 повідомлень за один раз. + Доступна панель чату + Користуватися застосунком однією рукою. + З\'єднуйтеся з друзями швидше. + Керуйте своєю мережею + Завантажено + Помилка скидання статистики + Перепідключити сервери? + Надіслані повідомлення + Статистика серверів буде скинута — це не можна буде відмінити! + Статус з\'єднання та серверів. + Докладна статистика + Отримати помилки + Надіслано безпосередньо + Надіслано загалом + Надіслано через проксі + Сервер SMP + Починаючи з %s. + Отримані повідомлення + Отримано загалом + Розмити медіа + Середній + Вимкнено + Слабке + Сильна + вимкнено + неактивний + Інформація про сервери + Файли + Ніякої інформації, спробуйте перезавантажити + Активні з\'єднання + Підключено + Підключені сервери + Підключення + Всього + Сесії передачі даних + Ви не підключені до цих серверів. Для доставки повідомлень до них використовується приватна маршрутизація. + Поточний профіль + Деталі + Помилки + Отримання повідомлень + Отримані повідомлення + В очікуванні + Проксіровані сервери + Показувати інформацію для + Починаючи з %s.\nУсі дані зберігаються приватно на вашому пристрої. + Перепідключити сервер для примусової доставки повідомлень. Це використовує додатковий трафік. + Скинути всю статистику + Скинути всю статистику? + Помилка + Помилка повторного підключення до сервера + Помилка повторного підключення до серверів + Перепідключити всі підключені сервери для примусової доставки повідомлень. Це використовує додатковий трафік. + Перепідключити всі сервери + Перепідключити сервер? + Помилки підтвердження + дублікати + інші помилки + Підтверджено + Підключення + помилки розшифрування + Видалено + Помилки підписки + Помилки видалення + Розмір + Підписано + Підписки проігноровані + Завантажені файли + Помилки завантаження + Завантажені файли + Помилки завантаження + Частини видалені + Частин завантажено + Частин вивантажено + Ця посилання було використано на іншому мобільному пристрої, створіть нове посилання на комп\'ютері. + Помилка копіювання + Будь ласка, перевірте, що мобільний пристрій і комп\'ютер підключені до однієї локальної мережі, і що брандмауер комп\'ютера дозволяє з\'єднання. +\nБудь ласка, повідомте про будь-які інші проблеми розробникам. + Налаштування + відеодзвінок + повідомлення + дзвінок + Зберегти розмову + відкрити + пошук + Ви все ще можете переглядати розмову з %1$s у списку чатів. + Контакт видалено! + Розмову видалено! + Видалити без сповіщення + Ви можете надсилати повідомлення %1$s з архівованих контактів. + Налаштовані SMP сервери + Ніяких відфільтрованих контактів + Інші SMP сервери + Ваші контакти + Інші XFTP сервери + Показати відсоток + Перевірити оновлення + Вимкнено + Завантаження оновлення додатку, не закривайте додаток + Завантажити %s (%s) + Відкрити розташування файлу + Пропустити цю версію + Доступні панелі додатка + Не можна зателефонувати контакту + Підключення до контакту, будь ласка, зачекайте або перевірте пізніше! + Дзвінки заборонені! + Вам необхідно дозволити контакту викликати вас, щоб ви могли самі їм дзвонити. + Надіслати повідомлення, щоб увімкнути дзвінки. + Не можна зателефонувати учаснику групи + Не можна надіслати повідомлення учаснику групи + спроби + XFTP сервер + Перепідключитися + Створено + закінчився + Захищений + інший + Проксірований + Надіслати помилки + Завершено + Усі профілі + Скинути + Вивантажено + Видалити %d повідомлень учасників? + Повідомлення будуть позначені для видалення. Одержувач(і) зможуть розкрити ці повідомлення. + Вибрати + Повідомлення будуть видалені для всіх учасників. + Вставити посилання + Вимкнути + Щоб отримувати повідомлення про нові випуски, увімкніть періодичну перевірку стабільної або бета-версії. + Продовжити + База даних чату експортована + Медіа та файлові сервери + Сервери повідомлень + SOCKS проксі + Деякі файли не були експортовані + Ви можете переїхати експортовану базу даних. + Ви можете зберегти експортований архів. + Запросити + Оновлення додатку завантажено + Встановлено успішно + Встановити оновлення + Будь ласка, перезапустіть додаток. + Скинути всі підказки + Доступно оновлення: %s + Завантаження оновлення скасовано + БАЗА ДАНИХ ЧАТУ + Вибрати профіль чату + Помилка при зміні профілю + Повідомлення будуть видалені — це не можна скасувати! + Видалити архів? + Поділитися профілем + Завантажений архів бази даних буде остаточно видалено з серверів. + Підключення було перенесено до %s, але під час перенаправлення на профіль сталася непередбачена помилка. + Режим системи + Не використовуйте облікові дані з проксі. + Аутентифікація проксі + Використовуйте різні облікові дані проксі для кожного з\'єднання. + Використовувати випадкові облікові дані + Ваші облікові дані можуть бути надіслані в незашифрованому вигляді. + Помилка під час збереження проксі + Переконайтеся, що конфігурація проксі правильна. + Пароль + Ім\'я користувача + Використовуйте різні облікові дані проксі для кожного профілю. + %1$d інша(і) помилка(и) файлу. + %1$d файл(и) не вдалося завантажити. + %1$s повідомлень не було переслано. + Завантажити + Переслати %1$s повідомлень? + Пересилаю повідомлення… + Переслати повідомлення без файлів? + Немає нічого для пересилання! + Зберігаю %1$s повідомлень. + %1$d помилка(и) файлу: +\n%2$s + %1$d файл(и) не були завантажені. + %1$d файл(и) ще завантажуються. + %1$d файл(и) були видалені. + Пересилаю %1$s повідомлень + Повідомлення були видалені після того, як ви їх вибрали. + Помилка при пересиланні повідомлень + Звук вимкнено + Помилка ініціалізації WebView. Переконайтеся, що WebView встановлено, і його підтримувана архітектура — arm64. \nПомилка: %s + Хвіст + Кут + Форма повідомлення + Сесія додатку + Нові облікові дані SOCKS будуть використовуватись щоразу, коли ви запускаєте додаток. + Нові облікові дані SOCKS будуть використовуватись для кожного сервера. + Сервер + Натисніть кнопку інформації поруч із полем адреси, щоб дозволити використання мікрофона. + Відкрийте Налаштування Safari / Сайти / Мікрофон, а потім виберіть \"Дозволити для localhost\". + Щоб здійснювати дзвінки, дозволіть використовувати ваш мікрофон. Завершіть дзвінок і спробуйте зателефонувати знову. + Кращі дзвінки + Краща безпека ✅ + Налаштовувана форма повідомлень. + Протоколи SimpleX перевірені компанією Trail of Bits. + Переключити аудіо та відео під час дзвінка. + Кращі дати повідомлень. + Кращий користувацький досвід + Видалити або модерувати до 200 повідомлень. + Переслати до 20 повідомлень одночасно. + Переключити профіль чату для одноразових запрошень. + Помилка прийняття умов + Сервери збереження помилок + Для профілю чату %s: + Ніяких медіа та файлових серверів. + Немає серверів повідомлень. + Немає серверів для маршрутизації приватних повідомлень. + Недоставлені повідомлення + Немає повідомлення + Це повідомлення було видалено або ще не отримано. + Поділіться одноразовим посиланням з другом + Поділіться адресою публічно + Ви можете задати ім\'я з\'єднання, щоб запам\'ятати, з ким ви поділилися посиланням. + Безпека з\'єднання + Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів. + Для соціальних мереж + Або поділитися приватно + Оператори серверів + Мережеві оператори + Умови будуть прийняті для ввімкнених операторів через 30 днів. + Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux. + Виберіть мережевих операторів для використання. + Ви можете налаштувати сервери за допомогою налаштувань. + Перегляньте пізніше + Оновлення + Прийняті умови + Умови будуть автоматично прийняті для увімкнених операторів: %s. + Оператор мережі + %s сервери + Вебсайт + Ваші сервери + Використовувати %s + Використовувати сервери + %s.]]> + %s.]]> + Прийняти умови + Переглянути умови + Додано сервери повідомлень + Для приватної маршрутизації + Для отримання повідомлень + Використовувати для файлів + Додано медіа та файлові сервери + Відкрити зміни + Відкриті умови + Сервери для нових файлів вашого поточного профілю чату + Для відправки + Помилка додавання сервера + Сервер оператора + Сервер додано до оператора %s. + Оператор сервера змінився. + Панелі інструментів додатків + Розмиття + Прозорість + Покращена навігація в чаті + - Відкрити чат на першому непрочитаному повідомленні.\n- Перейти до цитованих повідомлень. + Другий попередньо встановлений оператор у застосунку! + Переглянути оновлені умови + Налаштування адреси + %s.]]> + Адреса або одноразове посилання? + Умови використання + лише з одним контактом – поділіться особисто чи через будь-який месенджер.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s, прийміть умови використання.]]> + Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням: + Помилки в конфігурації серверів. + Умови приймаються з: %s. + Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих + Умови приймаються до: %s. + Продовжити + Створити одноразове посилання + Помилка оновлення сервера + для кращої конфіденційності метаданих. + Децентралізація мережі + Оператор + Немає серверів для отримання повідомлень. + SimpleX адреса або одноразове посилання? + Новий сервер + Немає серверів для отримання файлів. + Переглянути умови + Немає серверів для надсилання файлів. + Попередньо встановлені сервери + Протокол сервера змінено. + Поділіться адресою SimpleX у соціальних мережах. + SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер. + З\'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн. + Натисніть Створити адресу SimpleX у меню, щоб створити її пізніше. + Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові. + Використовувати для повідомлень + Ви можете налаштувати операторів у налаштуваннях Мережі та серверів. + Або імпортуйте архівний файл + Віддалені мобільні + Пристрої Xiaomi: будь ласка, увімкніть Автозапуск у налаштуваннях системи, щоб сповіщення працювали.]]> + Повідомлення занадто велике! + Будь ласка, зменшіть розмір повідомлення або видаліть медіа та надішліть знову. + Додайте учасників команди до розмови. + Бізнес адреса + Перевіряти повідомлення кожні 10 хвилин. + Без фонової служби + Сповіщення та батарея + Додаток завжди працює у фоні. + зашифрованими end-to-end, з пост-квантовою безпекою в особистих повідомленнях.]]> + Покинути чат? + Учасник буде видалений з чату — це неможливо скасувати! + Бізнес чати + Конфіденційність для ваших клієнтів. + Доступна панель чату + Додати друзів + Додати учасників команди + Запросити до чату + Чат буде видалений для всіх учасників — це неможливо скасувати! + Видалити чат + Видалити чат? + Тільки власники чату можуть змінювати налаштування. + Роль буде змінена на %s. Усі учасники чату отримають повідомлення. + Прямі повідомлення між учасниками заборонені. + %1$s.]]> + Чат вже існує! + Як це допомагає зберігати конфіденційність + Прямі повідомлення між учасниками заборонені в цьому чаті. + Покинути чат + Чат + Чат буде видалений для вас — це неможливо скасувати! + Будь ласка, зменшіть розмір повідомлення та надішліть знову. + Скопіюйте та зменшіть розмір повідомлення для відправки. + Ви припините отримувати повідомлення з цього чату. Історія чату буде збережена. + Ваш профіль буде надіслано учасникам чату. + Коли увімкнено більше ніж одного оператора, жоден з них не має метаданих, щоб дізнатися, хто спілкується з ким. + прийнято запрошення + запит на підключення + Про операторів + SimpleX Chat та Flux уклали угоду про включення серверів, що працюють на Flux, до додатку. + Підключення вимагає повторного узгодження шифрування. + Виконується повторне узгодження шифрування. + Виправити + Виправити підключення? + Увімкнути журнали + Помилка збереження бази даних + a + b + закреслити + Підключення не готове. + Відкрити за допомогою %s + Немає чатів у списку %s. + Немає непрочитаних чатів + всі + Зберегти список + Видалити + Видалити список? + Назва списку... + Усі чати буде видалено зі списку %s, а сам список видалено + Контакти + Помилка створення списку чату + Чати не знайдено + Створити список + Вибране + Без чатів + Редагувати + Помилка завантаження списків чату + Помилка оновлення списку чату + Групи + Компанії + Додати до списку + Додати список + Список + Назва списку та емодзі мають відрізнятися для всіх списків. + Нотатки + Список змін + Змінити порядок + Помилка збереження налаштувань + Помилка при створенні звіту + Тільки ви та модератори бачать це + Повідомити про спам: тільки модератори групи побачать це. + Порушення правил спільноти + модератор + Причина повідомлення? + Тільки відправник і модератори бачать це + Повідомити + Повідомити про порушення: тільки модератори групи побачать це. + Повідомлення буде архівоване для вас. + Інша причина + архівование повідомлення + Архів + Неприпустимий контент + Повідомити про контент: тільки модератори групи побачать це. + Архівувати повідомлення? + Повідомити про профіль учасника: тільки модератори групи побачать це. + Неприпустимий профіль + Спам + Повідомити інше: тільки модератори групи побачать це. + Архівувати повідомлення + архівоване повідомлення від %s + Видалити повідомлення + 1 повідомлення + %d повідомлень + Повідомлення учасників + Повідомлення + Вміст порушує умови використання + Спам + Файл заблоковано оператором сервера: \n%1$s. + Так + Відкрити посилання + Відкрити вебпосилання? + Підключення заблоковано + Запитати + Відкрити посилання зі списку чату + Підключення заблоковано оператором сервера: \n%1$s. + Ні + Назвати чат… + Повідомлення в цьому чаті ніколи не будуть видалені. + Видалити повідомлення чату з вашого пристрою. + Вимкнути автоматичне видалення повідомлень? + Вимкнути видалення повідомлень + 1 рік + типово (%s) + Змінити автоматичне видалення повідомлень? + Цю дію не можна скасувати — повідомлення, надіслані та отримані в цьому чаті раніше за обраний час, будуть видалені. + TCP-порт для повідомлень + Використовувати TCP-порт %1$s, якщо порт не вказано. + Використовувати веб-порт + Вимкнути звук + Непрочитані згадки + Ви можете згадувати до %1$s учасників у кожному повідомленні! + Усі звіти будуть архівовані для вас. + Архівувати всі звіти? + Архівувати %d звітів? + Архівувати звіти + Для всіх модераторів + Для мене + Звіт: %s + Заборонити повідомлення модераторам. + Повідомлення модераторам заборонено в цій групі. + Учасники можуть повідомляти повідомлення модераторам. + Дозволити повідомляти повідомлення модераторам. + відхилено + відхилено + Краща продуктивність груп + Не пропускайте важливі повідомлення. + Отримуйте сповіщення, коли вас згадують. + Допоможіть адміністраторам модерувати їхні групи. + Організовуйте чати в списки + Приватні імена медіа-файлів. + Надсилати приватні звіти + Краща конфіденційність та безпека + Швидша відправка повідомлень. + Згадуйте учасників 👋 + Встановлюйте термін придатності повідомлень у чатах. + Швидше видалення груп. + Помилка зчитування пароля бази даних + Пароль у сховищі ключів не можна зчитати. Це могло статися після оновлення системи, несумісного з додатком. Якщо це не так, зверніться до розробників. + Пароль у сховищі ключів не можна зчитати, введіть його вручну. Це могло статися після оновлення системи, несумісного з додатком. Якщо це не так, зверніться до розробників. + очікує + очікує на схвалення + Видалити учасників? + Усі нові повідомлення від цих учасників будуть приховані! + Заблокувати учасників для всіх? + Учасників буде видалено з групи – це неможливо скасувати! + модератори + Повідомлення від цих учасників буде показано! + Учасників буде видалено з чату – це неможливо скасувати! + Розблокувати учасників для всіх? + Оновлені умови + Приватні чати, групи та ваші контакти недоступні для операторів сервера. + Прийняти + Використовуючи SimpleX Chat, ви погоджуєтесь на:\n- надсилати тільки легальний контент у публічних групах.\n- поважати інших користувачів – без спаму. + Налаштувати операторів сервера + Політика конфіденційності та умови використання + Використовувати короткі посилання (BETA) + Це посилання вимагає новішої версії додатку. Будь ласка, оновіть додаток або попросіть вашого контакту надіслати сумісне посилання. + Повне посилання + Коротке посилання + Посилання на канал SimpleX + Несумісне посилання для підключення + Усі сервери + Ні + Типові сервери + Використовуйте TCP порт 443 лише для попередньо налаштованих серверів. + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index 939071da2b..3cbc54f652 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -7,10 +7,10 @@ Địa chỉ Hủy bỏ Chấp nhận - liên kết dùng một lần + đường dẫn dùng 1 lần Thêm liên hệ Thông tin về SimpleX Chat - Thêm máy chủ… + Thêm máy chủ Thông tin về SimpleX quản trị viên Thêm lời chào @@ -24,7 +24,7 @@ Chấp nhận Thêm máy chủ bằng cách quét mã QR. Cài đặt mạng nâng cao - Tất cả các cuộc hội thoại và tin nhắn sẽ bị xóa - quá trình này không thể được hoàn tác! + 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! Hủy bỏ việc đổi địa chỉ? Việc thay đổi địa chỉ sẽ bị hủy bỏ. Địa chỉ nhận cũ tiếp tục được sử dụng. 30 giây @@ -41,7 +41,7 @@ 1 tháng 1 tuần Hủy bỏ việc đổi địa chỉ - Các quản trị viên có thể tạo liên kết để tham gia nhóm. + Các quản trị viên có thể tạo đường dẫn để tham gia nhóm. Một ngày Biến thể của màu thứ cấp Tất cả các thành viên trong nhóm vẫn sẽ được giữ kết nối. @@ -74,7 +74,7 @@ Cho phép gửi tin nhắn tự xóa. Cho phép gửi tin nhắn thoại. Cho phép thả biểu tượng cảm xúc. - Cho phép gửi các tập tin, ảnh và video. + Cho phép gửi các tệp và đa phương tiện. Luôn luôn bật Tất cả liên hệ của bạn vẫn sẽ được giữ kết nối. 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. @@ -96,12 +96,12 @@ ỨNG DỤNG Di chuyển dữ liệu ứng dụng Sao lưu dữ liệu ứng dụng - Mật mã ứng dụng đã được thay thế bằng mật mã tự hủy. + Mã truy cập ứng dụng đã được thay thế bằng mã tự hủy. Ứng dụng mã hóa các tệp cục bộ mới (trừ video). Áp dụng - Mật mã ứng dụng + Mã truy cập ứng dụng BIỂU TƯỢNG ỨNG DỤNG - Mật mã ứng dụng + Mã truy cập Phiên bản ứng dụng: v%s Phiên bản ứng dụng 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. @@ -115,15 +115,14 @@ Tắt âm Yêu cầu để nhận hình ảnh cho mỗi hồ sơ trò chuyện bạn có trong ứng dụng.]]> - 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 cho từng liên hệ và thành viên nhóm. -\nXin lưu ý: 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. + cho từng liên hệ và thành viên nhóm. \nXin lưu ý: 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.]]> cuộc gọi thoại Các cuộc gọi thoại và video Các cuộc gọi thoại và video Các cuộc gọi thoại/video tất cả các thành viên Các cuộc gọi thoại/video bị cấm. - Cho phép gửi liên kết SimpleX. + Cho phép gửi đường dẫn SimpleX. quản trị viên tác giả Quá trình xác thực đã bị hủy bỏ @@ -137,7 +136,7 @@ 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. Tự động chấp nhận yêu cầu liên hệ Địa chỉ máy tính xấu - Thêm liên hệ: để tạo liên kết mời mới, hoặc kết nối qua liên kết bạn nhận được.]]> + Thêm liên hệ: để tạo đường dẫn mời mới, hoặc kết nối qua đường dẫn bạn nhận được.]]> hàm băm tin nhắn xấu " \nCó sẵn ở v5.1" @@ -170,6 +169,2197 @@ in đậm Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn tự xóa. Cả bạn và liên hệ của bạn đều có thể thả cảm xúc tin nhắn. - Xin lưu ý: 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 liên kết sử dụng kết nối trực tiếp.]]> - Tốt cho pin. Dịch vụ nền 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.]]> - \ No newline at end of file + Xin lưu ý: 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.]]> + Tốt cho pin. Ứ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.]]> + Luôn luôn + đang gọi… + Theo hồ sơ trò chuyện (mặc định) hoặc theo kết nối (BETA). + cuộc gọi đang chờ + Sử dụng nhiều pin hơn! Ứng dụng luôn luôn chạy - thông báo sẽ được hiển thị ngay lập tức.]]> + Cảnh báo: kho lưu trữ sẽ bị xóa.]]> + Cuộc gọi đang chờ + Cho phép hạ cấp + Cuộc gọi đã kết thúc! + Luôn luôn sử dụng định tuyến riêng tư. + Cuộc gọi kết thúc + cuộc gọi kết thúc %1$s + lỗi cuộc gọi + CUỘC GỌI + Hủy xem trước ảnh + Hủy xem trước tệp + Hủy + Camera + Camera hiện đang bận + Camera + Cuộc gọi trên màn hình khóa: + Đen + Áp dụng cho + Biến thể của màu sơ cấp 2 + Cài đặt nâng cao + Tất cả chế độ màu + Camera và mic + Không thể mời liên hệ! + Không thể truy cập Keystore để lưu mật khẩu cơ sở dữ liệu + hủy bỏ xem trước đường dẫn + Hủy bỏ di chuyển + Chủ đề ứng dụng + Không thể nhận tệp + Không thể khởi tạo cơ sở dữ liệu + đã hủy bỏ %s + Không thể mời liên hệ! + Hủy bỏ tin nhắn động + đã thay đổi chức vụ của %s thành %s + 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 đó. + đã thay đổi địa chỉ cho bạn + Thay đổi mật khẩu cơ sở dữ liệu? + đã thay đổi chức vụ của bạn thành %s + Thay đổi chế độ khóa + Di động + Đổi mật khẩu + Thay đổi chức vụ nhóm? + Thay đổi + Thay đổi địa chỉ nhận? + Thay đổi địa chỉ nhận + Thay đổi chế độ tự hủy + Thay đổi mã tự hủy + Thay đổi chức vụ + đang thay đổi địa chỉ… + đang thay đổi địa chỉ… + Bảng điều khiển trò chuyện + Kết nối trò chuyện đang hoạt động + Cơ sở dữ liệu trò chuyện đã bị xóa + 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. + Cơ sở dữ liệu trò chuyện đã được nhập + Kết nối trò chuyện đã được dừng lại + đang thay đổi địa chỉ cho %s… + Tùy chọn trò chuyện + Màu trò chuyện + CƠ SỞ DỮ LIỆU TRÒ CHUYỆN + Kết nối trò chuyện đã được dừng lại + Cơ sở dữ liệu đã được di chuyển! + Các cuộc trò chuyện + CÁC CUỘC TRÒ CHUYỆN + Kiểm tra tin nhắn mới mỗi 10 phút trong tối đa 1 phút + Giao diện Trung Quốc và Tây Ban Nha + Trò chuyện với nhà phát triển + Kiểm tra lại địa chỉ server và thử lại. + Chọn một tệp + Hồ sơ trò chuyện + Kiểm tra kết nối internet của bạn và thử lại + Chủ đề trò chuyện + Chế độ màu + Xác minh dọn dẹp + Dọn dẹp + Nút đóng + Dọn dẹp ghi chú riêng tư? + có màu + Dọn dẹp cuộc trò chuyện + Dọn dẹp + Sắp ra mắt! + Di chuyển từ một thiết bị kháctrên thiết bị mới và quét mã QR.]]> + Dọn dẹp + Dọn dẹp cuộc trò chuyện? + Cài đặt cấu hình cho các máy chủ ICE + Xác nhận các cài đặt mạng + Xác nhận mã truy cập + hoàn thành + Xác nhận mật khẩu + Xác nhận nâng cấp cơ sở dữ liệu + So sánh mã bảo mật với liên hệ của bạn. + Xác nhận + So sánh tệp + Xác nhận các tệp từ những máy chủ không xác định. + Xác nhận mật khẩu mới… + Xác nhận tải lên + Xác nhận rằng bạn nhớ mật khẩu cơ sở dữ liệu để chuyển nó đi. + Kết nối + đã kết nối + Đã kết nối + đã kết nối + Kết nối trực tiếp? + Kết nối + đã kết nối + Tự động kết nối + đã kết nối + Đã kết nối + Xác nhận thông tin đăng nhập của bạn + Kết nối + Máy tính đã được kết nối + đã kết nối trực tiếp + đang kết nối (đã được chấp nhận) + đang kết nối… + Đã kết nối tới điện thoại + Điện thoại đã được kết nối + đang kết nối + đang kết nối… + đang kết nối… + Kết nối ẩn danh + đang kết nối + Đã kết nối tới máy tính + đang kết nối (đã được thông báo) + đang kết nối… + đang kết nối (đã được giới thiệu) + đang kết nối (lời mời giới thiệu) + Kết nối tới máy tính đang ở trong tình trạng không tốt + Kết nối đã bị ngắt + Kết nối qua đường dẫn / mã QR + Kết nối tới chính bạn? + Yêu cầu kết nối đã được gửi! + Kết nối qua đường dẫn + Đang kết nối cuộc gọi + Kết nối đã bị ngắt + Đang kết nối tới máy tính + - 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. + Kết nối đã bị ngắt + %s đang ở trong tình trạng không tốt]]> + Kết nối tới máy tính + Thời gian chờ kết nối + Lỗi kết nối + Kết nối + Kết nối qua đường dẫn? + kết nối %1$d + kết nối đã được tạo lập + Kết nối với %1$s? + Lỗi kết nối (AUTH) + Kết nối + đang kết nối cuộc gọi… + Kết nối qua địa chỉ liên lạc? + Kết nối qua đường dẫn dùng một lần? + Liên hệ đã được kiểm tra + Các liên hệ + Liên hệ cho phép + Liên hệ đã tồn tại + liện hệ %1$s đã thay đổi thành %2$s + Liên hệ này vẫn chưa được kết nối! + Liên hệ ẩn: + 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! + Tên liên hệ + liên hệ có bảo mật đầu cuối + liên hệ không có bảo mật đầu cuối + Tùy chọn liên hệ + Sao chép + Tạo + Liên hệ có thể đánh dấu tin nhắn để xóa; bạn vẫn sẽ có thể xem được chúng. + Biểu tượng ngữ cảnh + Tiếp tục + Đóng góp + Đã sao chép vào bộ nhớ đệm + Phiên bản lõi: v%s + Sửa tên thành %s? + Sao chép lỗi + Tạo nhóm bằng một hồ sơ ngẫu nhiên. + Tạo hồ sơ mới trong ứng dụng trên máy tính. 💻 + Không thể gửi tin nhắn + Tạo tệp + Tạo một địa chỉ để cho mọi người kết nối với bạn. + Tạo đường dẫn nhóm + Tạo đường dẫn + Được tạo ra tại + Tạo địa chỉ + Được tạo ra tại: %s + Tạo hồ sơ trò chuyện + Tạo nhóm + Chủ đề tối + Mật khẩu hiện tại… + Cơ sở dữ liệu đã được mã hóa! + Mật khẩu mã hóa cơ sở dữ liệu sẽ đượ cập nhật và lưu trữ trong Keystore. + (hiện tại) + Tối + Các màu chế độ tối + Tối + Lỗi nghiêm trọng + Tạo đường dẫn lời mời dùng một lần + Đang tạo đường dẫn… + Tạo hồ sơ + Tùy chỉnh và chia sẻ các chủ đề màu sắc. + Các chủ đề tùy chỉnh + Kích cỡ tệp hiện đang được hỗ trợ tối đa là %1$s. + Tạo hồ sơ của bạn + Người sáng tạo + Tạo nhóm bí mật + Mã truy cập hiện tại + Tạo nhóm bí mật + 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. + Thời lượng tùy chỉnh + Mật khẩu mã hóa cơ sở dữ liệu sẽ được cập nhật. + Tùy chỉnh chủ đề + Tạo địa chỉ SimpleX + Tạo hồ sơ + Tạo hàng đợi + Tạo đường dẫn lưu trữ + tùy chỉnh + Chủ đề tối + Hạ cấp cơ sở dữ liệu + %d liên hệ đã được chọn + ID cơ sở dữ liệu: %d + Cơ sở dữ liệu sẽ được mã hóa và mật khẩu thì được lưu trữ trong Keystore. + Nâng cấp cơ sở dữ liệu + 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 + ID cơ sở dữ liệu + ngày + 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. + 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ó. + Mật khẩu cơ sở dữ liệu + 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. + %d ngày + Các ID cơ sở dữ liệu và tùy chọn cách ly truyền tải. + %dd + %d ngày + Cơ sở dữ liệu sẽ được mã hóa. + 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. + Mật khẩu cơ sở dữ liệu và xuất dữ liệu + Mật khẩu cơ sở dữ liệu khác với mật khẩu được lưu trong Keystore. + Mật khẩu cơ sở dữ liệu là cần thiết để mở kết nối trò chuyện. + Lỗi cơ sở dữ liệu + Xóa + Xóa địa chỉ? + Xóa hồ sơ trò chuyện? + Xóa %d tin nhắn? + Xóa địa chỉ + Phi tập trung + Xóa + Xóa cơ sở dữ liệu khỏi thiết bị này + Xóa sau + đã xóa liên hệ + Gỡ lỗi truyền tải + Đã xóa vào: %s + Lỗi chuyển đổi + Xóa và thông báo tới liên hệ + Xóa liên hệ + Xóa + Xóa cơ sở dữ liệu + Xóa tất cả các tệp + Xóa tệp và đa phương tiện? + mặc định (%s) + đã xóa nhóm + Xóa tệp + Xóa liên hệ? + Đã xóa vào + Lỗi giải mã + đã xóa + Xóa hồ sơ trò chuyện + Xóa hồ sơ trò chuyện? + Xóa tin nhắn thành viên? + Xóa chỉ mình tôi + Xóa nhóm + Xóa kết nối đang chờ? + Xóa tệp cho tất cả các hồ sơ trò chuyện + Xóa đường dẫn? + Xóa hồ sơ + Xóa cho mọi người + Xóa đường dẫn + Xóa ảnh + Xóa các tin nhắn + Xóa nhóm? + Xóa tin nhắn? + 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. + Máy tính đang bận + Máy tính đang không hoạt động + Máy tính đã bị ngắt kết nối + Quét mã QR.]]> + Xóa máy chủ + Xóa hàng đợi + Công cụ nhà phát triển + THIẾT BỊ + Tùy chọn cho nhà phát triển + Xác thực thiết bị đã bị vô hiệu hóa. Tắt Khóa SimpleX. + Lỗi máy chủ đích: %1$s + Chỉ báo đã nhận! + Chỉ báo đã nhận bị vô hiệu hóa! + Máy tính có mã mời sai + Mô tả + Chuyển gửi + Các thiết bị máy tính + Máy tính + Địa chỉ máy tính + 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ị. + Đã xác nhận + Các kết nối đang hoạt động + Tất cả hồ sơ + Beta + Kiểm tra cập nhật + Đang kết nối + Các máy chủ SMP đã được cấu hình + Các máy chủ XFTP đã được cấu hình + Bản cập nhật ứng dụng đã được tải xuống + Kiểm tra cập nhật + Đã kết nối + thử + Các máy chủ đã kết nối + Lỗi xác nhận + Đã hoàn thành + Các khối đã bị xóa + Các khối đã được tải xuống + Các khối đã được tải lên + Hồ sơ hiện tại + lỗi giải mã + Thống kê chi tiết + Chi tiết + Các kết nối + Đã tạo + Đã xóa + Lỗi xóa + %d sự kiện nhóm + Tin nhắn trực tiếp giữa các thành viên bị cấm trong nhóm này. + %d tệp với tổng kích thước là %s + phần di dời khác nhau trong ứng dụng/cơ sở dữ liệu: %s / %s + Tin nhắn trực tiếp + %dh + %d giờ + %d giờ + Tên, hình đại diện và cách ly truyền tải khác nhau. + Thiết bị + trực tiếp + 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ị. + Tắt chỉ báo đã nhận? + Tin nhắn tự xóa + Tin nhắn tự xóa + Ngắt kết nối + Tắt (giữ thông tin ghi đè) + Biến mất vào lúc: %s + Tin nhắn tự xóa + Tắt cho tất cả + Tắt cho tất cả các nhóm + Biến mất vào lúc + Ngắt kết nối + Tắt thông báo + Tắt SimpleX Lock + Tắt (giữ thông tin ghi đè về nhóm) + Tin nhắn tự xóa bị cấm trong cuộc hội thoại này. + Tắt + Tắt chỉ báo đã nhận cho nhóm? + đã bị tắt + Đã bị tắt + Tắt + đã bị tắt + Tin nhắn tự xóa bị cấm. + gọi + kết nối + Liên hệ sẽ bị xóa - điều này không thể hoàn tác! + Cuộc trò chuyện đã bị xóa! + Làm mờ đa phương tiện + Không thể gọi liên hệ + Cho phép thực hiện cuộc gọi? + Cuộc gọi bị cấm! + Đang kết nối tới liên hệ, xin vui lòng đợi hoặc kiểm tra sau! + Liên hệ đã bị xóa. + Không thể nhắn tin cho thành viên nhóm + Không thể gọi thành viên nhóm + Xác nhận xóa liên hệ? + Liên hệ đã bị xóa! + Các liên hệ được lưu trữ + Tạo + Mờ hình ảnh để riêng tư hơn. + Kiểm soát mạng của bạn + Kết nối và trạng thái máy chủ. + Cài đặt nâng cao + Bất kỳ ai cũng có thể tạo máy chủ. + Tiếp tục + Kết nối nhanh hơn với bạn bè. + Cơ sở dữ liệu trò chuyện đã được xuất + Lưu trữ các liên hệ để trò chuyện sau. + Ngắt kết nối máy tính? + Xóa tối đa 20 tin nhắn cùng một lúc. + Đã ngắt kết nối với lý do:%s + %s với lý do: %s]]> + Ngắt kết nối các thiết bị di động + Đã ngắt kết nối + Đị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. + 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. + Xóa mà không thông báo + Có thể tìm thấy qua mạng cục bộ + Xóa %d tin nhắn của các thành viên? + %d tháng + %dm + KHÔNG sử dụng định tuyến riêng tư. + Khám phá và tham gia nhóm + %dmth + Không gửi lịch sử đến các thành viên mới. + %d phút + 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ư. + %d tháng + %d phút + %d tin nhắn bị chặn + %d tin nhắn bị chặn bởi quản trị viên + %d tin nhắn được đánh dấu là đã xóa + Tên hiển thị không thể chứa khoảng trắng. + Khám phá qua mạng cục bộ + Tải về không thành công + Không tạo địa chỉ + Không hiển thị lại + Tải về tệp tin + Đang tải về kho lưu trữ + Hạ cấp và mở kết nối trò chuyện + Tải về + Không bật + Đã tải về + Đang tải về bản cập nhật ứng dụng, đừng đóng ứng dụng + Các tệp đã tải về + Lỗi tải về + mã hóa đầu cuối + Tải xuống các phiên bản mới từ GitHub. + %d giây + %ds + Đang tải xuống chi tiết đường dẫn + Tên hiển thị trùng lặp! + %d tuần + %d tuần + %d giây + tin nhắn trùng lặp + %dw + cuộc gọi thoại mã hóa đầu cuối + các bản sao + Tải xuống %s (%s) + đã bật cho liên hệ + Tai nghe + Bật cho tất cả các nhóm + Chỉnh sửa hình ảnh + đã chỉnh sửa + Bật cho tất cả + đã bật + đã bật cho bạn + Đã bật cho + Cho phép truy cập camera + cuộc gọi video mã hóa đầu cuối + Cho phép nhận cuộc gọi từ màn hình khóa thông qua Cài đặt. + Cho phép + Chỉnh sửa hồ sơ nhóm + Cho phép xóa tin nhắn tự động? + Chỉnh sửa + Thư điện tử + Lỗi: %1$s + Lỗi + Tái đàm phán mã hóa thất bại. + mã hóa ok + cho phép tái đàm phán mã hóa với %s + cần tái đàm phán mã hóa cho %s + Bật chỉ báo đã nhận? + Mã hóa các tệp và đa phương tiện được lưu trữ + Lỗi + Kết thúc cuộc gọi + Bật trong cuộc trò chuyện trực tiếp (BETA)! + Bật mã truy cập tự hủy + Mã hóa cơ sở dữ liệu? + Cơ sở dữ liệu được mã hóa + Lỗi tái đàm phán mã hóa + Nhập mật khẩu + Bật (giữ thông tin ghi đè) + Mã hóa + mã hóa đã đồng nhất + cho phép tái đàm phán mã hóa + cần tái đàm phán mã hóa + Bật TCP keep-alive + Nhập mật khẩu trong tìm kiếm + Nhập tên của thiết bị này… + Bật SimpleX Lock + Nhập lời chào… + Nhập mã truy cập + Lỗi + Nhập lời chào... (không bắt buộc) + đã kết thúc + mã hóa đã đồng nhất cho %s + mã hóa ok cho %s + lỗi + Nhập máy chủ thủ công + Bật (giữ thông tin ghi đè nhóm) + Nhập đúng mật khẩu. + Nhập mật khẩu… + Mã hóa các tệp cục bộ + Bật tự hủy + Bật chỉ báo đã nhận cho các nhóm? + Nhập tên nhóm: + Nhập tên của bạn: + Bật khóa + Lỗi + Lỗi tạo hồ sơ! + Lỗi thay đổi cài đặt + Lỗi chặn thành viên cho tất cả + Lỗi thêm thành viên + Lỗi tạo tin nhắn + Lỗi tạo địa chỉ + Lỗi chấp nhận yêu cầu liên hệ + Lỗi thay đổi địa chỉ + Lỗi xóa cơ sở dữ liệu trò chuyện + Lỗi tạo liên hệ thành viên + Lỗi hủy bỏ thay đổi địa chỉ + Lỗi tạo đường dẫn nhóm + Lỗi thay đổi chức vụ + Lỗi kết nối đến máy chủ chuyển tiếp %1$s. Vui lòng thử lại sau. + Lỗi xóa liên hệ + Lỗi xóa nhóm + Lỗi xóa đường dẫn nhóm + Lỗi xóa yêu cầu liên hệ + Lỗi xóa cơ sở dữ liệu + Lỗi xóa kết nối liên hệ đang chờ xử lý + Lỗi xuất cơ sở dữ liệu trò chuyện + Lỗi mã hóa cơ sở dữ liệu + Lỗi bật chỉ báo đã nhận! + Lỗi xóa ghi chú riêng tư + Lỗi xóa hồ sơ người dùng + Lỗi nhập cơ sở dữ liệu trò chuyện + Lỗi xuất cơ sở dữ liệu trò chuyện + Lỗi tải xuống kho lưu trữ + 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 + Lỗi xóa thành viên + Lỗi: %s + Lỗi mở trình duyệt + Lỗi tham gia nhóm + Lỗi tải thông tin chi tiết + Lỗi nhận tệp + Lỗi lưu hồ sơ nhóm + Lỗi lưu tệp + Lỗi tải máy chủ SMP + Lỗi tải máy chủ XFTP + Lỗi kết nối lại máy chủ + Lỗi kết nối lại máy chủ + Lỗi + Lỗi khôi phục thống kê + Lỗi cập nhật đường dẫn nhóm + lỗi hiển thị tin nhắn + lỗi hiển thị nội dung + Lỗi lưu máy chủ ICE + Lỗi lưu máy chủ XFTP + Lỗi lưu máy chủ SMP + Lỗi gửi tin nhắn + Lỗi khởi động kết nối trò chuyện + Lỗi dừng kết nối trò chuyện + Lỗi hiển thị thông báo, liên hệ với nhà phát triển. + Lỗi lưu mật khẩu người dùng + Lỗi lưu cài đặt + Lỗi gửi lời mời + Lỗi chuyển đổi hồ sơ! + Lỗi đồng bộ kết nối + Lỗi cài đặt địa chỉ + Dù đã tắt trong cuộc trò chuyện. + Lỗi cập nhật cấu hình mạng + Lỗi cập nhật quyền riêng tư người dùng + Mở rộng chọn chức vụ + THỬ NGHIỆM + Mở rộng + Thoát mà không lưu + đã hết hạn + Lỗi xác thực mật khẩu: + Quá trình thực hiện chức năng mất quá nhiều thời gian: %1$dgiây: %2$s + Tính năng thử nghiệm + Xuất cơ sở dữ liệu + Lỗi tải lên kho lưu trữ + Tập tin đã xuất không tồn tại + TẬP TIN + Không thể tải các cuộc trò chuyện + Không tìm thấy tệp - có thể tập tin đã bị xóa và hủy bỏ. + Lỗi tệp + Xuất chủ đề + Không thể tải cuộc trò chuyện + Tập tin + Nhanh chóng và không cần phải đợi người gửi hoạt động! + Không tìm thấy tệp + Tập tin: %s + Tham gia nhanh chóng hơn và xử lý tin nhắn ổn định hơn. + Tập tin + Yêu thích + Tập tin + Lỗi máy chủ tệp: %1$s + Trạng thái tệp + Tệp và đa phương tiện không được cho phép + Tệp và đa phương tiện bị cấm. + Tệp sẽ bị xóa khỏi máy chủ. + Tệp & đa phương tiện + Tệp và đa phương tiện bị cấm! + 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. + Tệp và đa phương tiện + Tệp đã bị xóa hoặc đường dẫn không hợp lệ + Tệp đã được lưu + 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! + Trạng thái tệp: %s + Lấp đầy + CƠ SỞ DỮ LIỆU TRÒ CHUYỆN + Lỗi chuyển đổi hồ sơ + 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. + Cuối cùng, chúng ta đã có chúng! 🚀 + Hoàn tất quá trình di chuyển ở thiết bị khác. + Hoàn tất quá trình di chuyển + Tìm các cuộc trò chuyện nhanh hơn + 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. + Sửa + Kích thước phù hợp + Chuyển tiếp + Đã được chuyển tiếp từ + đã được chuyển tiếp + Máy chủ chuyển tiếp: %1$s +\nLỗi máy chủ đích: %2$s + Máy chủ chuyển tiếp: %1$s +\nLỗi: %2$s + Chuyển tiếp và lưu tin nhắn + Chức năng sửa không hỗ trợ bởi thành viên nhóm + Cho tất cả mọi người + Đã được chuyển tiếp + Không sử dụng thông tin đăng nhập với proxy. + Sửa kết nối + Sửa kết nối? + Chức năng sửa không hỗ trợ bởi liên hệ + Sửa mã hóa sau khi hồi phục dữ liệu dự phòng. + Lỗi lưu proxy + Đổi máy ảnh + Kích thước font + %1$d lỗi tệp khác. + Lỗi chuyển tiếp tin nhắn + %1$d tệp tải không thành công. + %1$d tệp đã bị xóa. + %1$d tệp đã không được tải xuống. + Tải xuống + Chuyển tiếp %1$s tin nhắn? + Chuyển tiếp tin nhắn… + %1$d lỗi tệp: +\n%2$s + %1$d tệp đang được tải xuống. + %1$s tin nhắn không được chuyển tiếp + Đang chuyển tiếp %1$s tin nhắn + 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. + Địa chỉ máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s. + 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. + CHO CONSOLE + Chuyển tiếp tin nhắn… + Giảm thiểu sử dụng pin hơn nữa + Chuyển tiếp tin nhắn mà không có tệp? + Chào buổi chiều! + Giao diện tiếng Pháp + Đã tìm thấy máy tính + Toàn bộ đường dẫn + Từ Thư viện + Tên đầy đủ: + Tuyệt đối phi tập trung - chỉ hiển thị cho thành viên. + Chào buổi sáng! + nhóm đã bị xóa + Các thành viên có thể thả cảm xúc tin nhắn. + 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ờ) + Các thành viên có thể gửi tin nhắn thoại. + Cấp quyền + Đường dẫn nhóm + Nhóm + Tên đầy đủ nhóm: + Các thành viên có thể gửi tin nhắn tự xóa. + Các thành viên có thể gửi tệp và đa phương tiện. + Các thành viên có thể gửi đường dẫn SimpleX. + Nhóm không hoạt động + Nhóm đã tồn tại rồi! + Các thành viên có thể gửi tin nhắn trực tiếp. + Đường dẫn nhóm + Lời mời nhóm đã hết hạn + 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. + Cho phép thực hiện cuộc gọi + Cấp quyền trong cài đặt + Ẩn + Ngắt kết nối + Ẩn + Mật khẩu hồ sơ ẩn + 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ủ. + hồ sơ nhóm đã được cập nhật + trợ giúp + Ẩn: + Xin chào! +\nKết nối với tôi qua SimpleX Chat: %s + Ẩn hồ sơ + TRỢ GIÚP + 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! + Nhóm sẽ bị xóa cho bạn - điều này không thể hoàn tác! + Tùy chọn nhóm + Lịch sử không được gửi đến các thành viên mới. + Ẩn màn hình ứng dụng trong danh sách các ứng dụng gần đây. + Quản trị nhóm + Lời chào nhóm + Tai nghe + Ẩn liên hệ và tin nhắn + Ẩn + Ẩn + Lịch sử + Không tìm thấy nhóm! + Hồ sơ trò chuyện ẩn + Cách sử dụng + Cách thức hoạt động + Cách thức SimpleX hoạt động + Cách làm + Lỗi khởi động WebView. Hãy đảm bảo bạn đã cài đặt WebView và kiến trúc hỗ trợ của nó là arm64.\nLỗi: %s + giờ + Lưu trữ + Phiên làm việc trên ứng dụng + Cách sử dụng markdown + Nhấn nút thông tin gần trường địa chỉ để cho phép sử dụng microphone. + Trải nghiệm cuộc gọi tốt hơn + Bảo mật hơn ✅ + Trải nghiệm người dùng tuyệt vời hơn + Hình dạng tin nhắn có thể tùy chỉnh được. + Mô tả thời gian tin nhắn tốt hơn. + Xóa hay kiểm duyệt tối đa 200 tin nhắn. + Góc + Chuyển tiếp tối đa 20 tin nhắn cùng một lúc. + Cách sử dụng máy chủ của bạn + Giao diện Hungary và Thổ Nhĩ Kỳ + Miễn nhiễm với tin nhắn rác + Nhập cơ sở dữ liệu trò chuyện? + Nếu bạn nhập mã tự hủy của mình khi mở ứng dụng: + Máy chủ ICE (một dòng mỗi máy) + 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! + 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! + Ngay lập tức + 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. + Hình ảnh + 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. + Hình ảnh đã được gửi + Hình ảnh + Hình ảnh đã được lưu vào Thư viện + 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: + quét mã QR trong cuộc gọi video, hoặc liên hệ của bạn có thể chia sẻ một đường dẫn mời.]]> + 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. + Nếu bạn chọn từ chối người gửi sẽ KHÔNG được thông báo. + cho liên hệ của bạn xem mã QR trong cuộc gọi video, hoặc chia sẻ đường dẫn.]]> + Nhập + Bỏ qua + không hoạt động + Nhập dữ liệu không thành công + Đã cải thiện việc chuyển gửi tin nhắn + Lỗi nhập chủ đề + Chế độ ẩn danh + Ẩn danh + Đang nhập dữ liệu từ kho lưu trữ + Nhập chủ đề + Đã cải thiện cấu hình máy chủ + Nhóm ẩn danh + 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ệ. + Đã cải thiện việc chuyển gửi tin nhắn + Nhập cơ sở dữ liệu + Âm thanh trong cuộc gọi + Nâng cao bảo mật và sự riêng tư + ẩn danh qua đường dẫn dùng một lần + Thông tin + gián tiếp (%1$s) + Để tiếp tục, hãy ngắt kết nối trò chuyện. + Cài đặt SimpleX Chat cho cửa sổ câu lệnh + ẩn danh qua đường dẫn địa chỉ liên lạc + Cuộc gọi thoại đến + Chức vụ ban đầu + ẩn danh qua đường dẫn nhóm + Mã bảo mật không đúng! + Trả lời đến + Phiên bản cơ sở dữ liệu không tương thích + Ngay lập tức + Mã truy cập không đúng + Tăng cỡ chữ. + (mới)]]> + Đã cài đặt thành công + Cài đặt cập nhật + Cuộc gọi video đến + Phiên bản không tương thích + MÀU SẮC GIAO DIỆN + đã được mời + Đường dẫn không hợp lệ + cuộc trò chuyện không hợp lệ + dữ liệu không hợp lệ + định dạng tin nhắn không hợp lệ + Mời + Lời mời đã hết hạn! + Đường dẫn không hợp lệ! + Đường dẫn không hợp lệ + Tên hiển thị không hợp lệ! + Đường dẫn kết nối không hợp lệ + Thông báo tức thời! + Mã QR không hợp lệ + Thông báo tức thời + Thông báo tức thời đã bị tắt! + Đường dẫn tệp không hợp lệ + Lỗi nội bộ + Mã QR không hợp lệ + Địa chỉ máy chủ không hợp lệ! + Xác nhận di dời không hợp lệ + Tên không hợp lệ! + Mời + lời mời tham gia nhóm %1$s + Mời thành viên + Tên cục bộ + Cảnh báo chuyển gửi tin nhắn + Đả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. + Đả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. + Liên kết với điện thoại + Thành viên không hoạt động + Tin nhắn đã được chuyển tiếp + Tin nhắn có thể được gửi sau nếu thành viên hoạt động + Đảm bảo cấu hình proxy là chính xác. + Đang tham gia nhóm + Sáng + Sáng + Các thiết bị di động đã được liên kết + Tham gia nhóm của bạn? + UI Nhật Bản và Bồ Đào Nha + được đánh dấu là đã xóa + TRỰC TIẾP + Tham gia + Tham gia ẩn danh + Bản nháp tin nhắn + Rời nhóm? + Mời thành viên + Mời vào nhóm + Rời nhóm + THÀNH VIÊN + Thông tin hàng đợi tin nhắn + Chỉ dữ liệu hồ sơ cục bộ + Giữ lại các kết nối của bạn + Làm cho một tin nhắn biến mất + Tin nhắn động! + Giữ lại lời mời chưa sử dụng? + Dự phòng định tuyến tin nhắn + Mời bạn bè + Đánh dấu đã xác thực + Đ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. + Tham gia nhóm? + Liên kết ứng dụng trên điện thoại và máy tính! 🔗 + thành viên %1$s đã đổi thành %2$s + Tạo kết nối riêng tư + Tạo hồ sơ riêng tư! + Đả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. + 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ũ. + Chế độ khóa + Giao diện tiếng Ý + thiết bị này v%s)]]> + Thả biểu tượng cảm xúc tin nhắn bị cấm. + đã được mời để kết nối + Tìm hiểu thêm + Giữ + Rời + Đăng nhập bằng thông tin xác thực của bạn + Lỗi chuyển gửi tin nhắn + tham gia với tư cách %s + Trợ giúp markdown + Sử dụng markdown trong tin nhắn + Tham gia nhóm? + k + Đang tải tệp + Các máy tính đã được liên kết + Đang tải các cuộc trò chuyện… + tin nhắn + Đánh dấu chưa đọc + Tối đa 40 giây, được nhận ngay lập tức. + Chỉ báo đã nhận tin nhắn! + Tệp lớn! + Đánh dấu đã đọc + Trung bình + Tin nhắn động + Giữ lại cuộc trò chuyện + Hình ảnh xem trước của đường dẫn + Thành viên sẽ bị xóa khỏi nhóm - việc này không thể được hoàn tác! + Chế độ sáng + UI tiếng Litva + TIN NHẮN VÀ TỆP + Việc xóa tin nhắn mà không thể phục hồi là bị cấm. + Tham gia vào các cuộc trò chuyện nhóm + Chế độ định tuyến tin nhắn + Hãy trò chuyện trên SimpleX Chat + in nghiêng + Nó ảnh hưởng đến pin như thế nào + 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. + Biểu đạt cảm xúc tin nhắn bị cấm trong cuộc trò chuyện này. + Bản nháp tin nhắn + Nó bảo vệ địa chỉ IP và các kết nối của bạn. + Khóa sau + Tin nhắn + Lỗi keychain + đã rời + đã được mời qua đường dẫn nhóm của bạn + thành viên + đã rời + 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 + Đả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ủ đề. + Menu và cảnh báo + Cảm xúc tin nhắn + Cảm xúc tin nhắn + Làm cho các cuộc trò chuyện của bạn trở nên khác biệt! + Tiếp nhận tin nhắn + Cài đặt máy tính đã được liên kết + đã được mời %1$s + Xóa tin nhắn mà không thể phục hồi + Tin nhắn + Máy chủ tin nhắn + Máy chủ tệp và đa phương tiện + Nội dung tin nhắn + Tin nhắn đã bị xóa sau khi bạn chọn chúng. + Tin nhắn từ %s sẽ được hiển thị! + Trạng thái tin nhắn + Tin nhắn đã được nhận + Tin nhắn đã được gửi + Trạng thái tin nhắn: %s + Tin nhắn quá lớn + mã hóa đầu cuốivớ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.]]> + Tin nhắn sẽ bị xóa - việc này không thể được hoàn tác! + Di chuyển từ một thiết bị khác + Tin nhắn sẽ bị xóa - việc này không thể được hoàn tác! + 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. + mã hóa đầu cuối kháng lượng tử 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.]]> + Mic + Hình dạng tin nhắn + Nguồn tin nhắn vẫn còn riêng tư. + Di chuyển thiết bị + 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. + Di chuyển tới đây + tháng + Nhiều hồ sơ trò chuyện + Thiết bị di động mới + Không bao giờ + Trải nghiệm trò chuyện mới 🎉 + Ứng dụng máy tính mới! + Mạng & máy chủ + cuộc gọi nhỡ + Mã truy cập mới + Tên hiển thị mới: + Kết nối mạng + Trạng thái mạng + Chủ đề trò chuyện mới + đã được kiểm duyệt bởi %s + đã được kiểm duyệt + Đã được kiểm duyệt vào + Đã được kiểm duyệt vào: %s + %s đã bị ngắt kết nối]]> + Mở trong ứng dụng di động, sau đó nhấn Kết nối trong ứng dung.]]> + Tin nhắn mới + Đã tắt thông báo khi không hoạt động! + Mới trong %s + - 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! + Kết nối mạng ổn định hơn. + Quản lý mạng + %s bị thiếu]]> + %s đang bận]]> + Di chuyển sang một thiết bị khác + Đang di chuyển + Hơn nữa + tin nhắn mới + Khả năng cao liên hệ này đã xóa kết nối với bạn. + Sự cố mạng - tin nhắn đã hết hạn sau nhiều lần cố gắng gửi đi. + Di chuyển: %s + Tắt thông báo + Yêu cầu liên lạc mới + Kiểm duyệt + không bao giờ + Tắt thông báo + Cuộc trò chuyện mới + phút + Kho lưu trữ cơ sở dữ liệu mới + Chức vụ thành viên mới + Nhiều cải tiến hơn nữa sắp ra mắt! + %s 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ị.]]> + Các tùy chọn đa phương tiện mới + Cuộc gọi nhỡ + Nhiều cải tiến hơn nữa sắp ra mắt! + Di chuyển sang một thiết bị khác qua mã QR. + Quá trình di chuyển hoàn tất + %s đang không hoạt động]]> + %s đã bị ngắt kết nối]]> + Không có thông tin chuyển gửi + 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. + Không có liên hệ để thêm + Không có thiết bị di động nào được kết nối + không + không có thông tin + Mật khẩu mới… + 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. + Không + Không có liên hệ nào được chọn + Không có cuộc gọi nền + Không có mã truy cập ứng dụng + Không có cuộc trò chuyện nào được lọc + Không có lịch sử + Không có liên hệ nào được lọc + không + Không + Thông tin xác thực SOCKS mới sẽ được sử dụng cho mỗi máy chủ. + không có mã hóa đầu cuối + Không có thông tin, hãy thử tải lại + bật + Không có gì để chuyển tiếp! + Thông báo sẽ dừng hoạt động cho đến khi bạn khởi động lại ứng dụng + Không có thông tin định danh người dùng. + không có văn bản + tắt + tắt` + Chỉ bạn mới có thể thực hiện cuộc gọi. + Chỉ liên hệ của bạn mới có thể thả cảm xúc tin nhắn. + Chỉ có thể gửi 10 video cùng một lúc + Đường dẫn lời mời dùng một lần + (chỉ được lưu trữ bởi thành viên nhóm) + Xem trước thông báo + Dịch vụ thông báo + Tắt + Không có tệp nào được gửi hay được nhận + quan sát viên + Không có kết nối mạng + 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. + Chỉ có thể gửi 10 hình ảnh cùng một lúc + Chỉ một thiết bị mới có thể hoạt động cùng một lúc + Không tương thích! + Thông báo + OK + Chỉ xóa cuộc trò chuyện + Chỉ bạn mới có thể gửi tin nhắn thoại. + 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 + Chỉ chủ nhóm mới có thể bật tính năng tin nhắn thoại. + Chỉ bạn mới có thể gửi tin nhắn tự xóa. + Đường dẫn lời mời dùng một lần + Chỉ có bạn mới có thể thả cảm xúc tin nhắn. + Thông báo sẽ chỉ được gửi cho đến khi ứng dụng dừng! + 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ờ) + Bản lưu trữ cơ sở dữ liệu cũ + được đề nghị %s + được đề nghị %s: %2s + Tắt + Chỉ chủ nhóm mới có thể điều chỉnh các tùy chọn nhóm. + 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) + Không có cuộc trò chuyện nào được chọn + Không có gì được chọn + Mở + Mở bảng điều khiển trò chuyện + Thay đổi hồ sơ trò chuyện + 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. + Mở + Mở thư mục cơ sở dữ liệu + Chỉ liên hệ của bạn mới có thể gửi tin nhắn tự xóa. + Khác + Hoặc quét mã QR + Dịch vụ onion sẽ được sử dụng khi có sẵn. + Chỉ liên hệ của bạn mới có thể gửi tin nhắn thoại. + chủ sở hữu + Mở cài đặt máy chủ + 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 đỏ. + 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ờ) + Mở cài đặt ứng dụng + Mục mã truy cập + Mở màn hình di chuyển + Dịch vụ onion sẽ không được sử dụng. + Đang mở cơ sở dữ liệu… + Hoặc hiển thị mã này + Chỉ liên hệ của bạn mới có thể thực hiện cuộc gọi. + mở + - 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! + Mở cài đặt + Hoặc chia sẻ đường dẫn tệp này một cách an toàn. + Mở SimpleX Chat để chấp nhận cuộc gọi + Mở Cài đặt Safari / Trang Web / Mic, rồi chọn Cho phép với localhost. + Mã truy cập + Mở cuộc trò chuyện + Mở vị trí tệp + Sử dụng từ máy tính trong ứng dụng di động và quét mã QR.]]> + Hoặc dán đường dẫn lưu trữ + các chủ sở hữu + Mở cổng trong tường lửa + Mã truy cập đã được đổi! + Mở nhóm + khác + các lỗi khác + Các máy chủ SMP khác + Các máy chủ XFTP khác + Dán đường dẫn mà bạn nhận được để kết nối với liên hệ của bạn… + Dán đường dẫn + Mật khẩu + Định kỳ + Đang chờ xử lý + Đang chờ xử lý + 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. + Thành viên trước đây %1$s + Dán đường dẫn để kết nối! + Thông báo định kỳ + Dán đường dẫn mà bạn nhận được + Dán + ngang hàng + Cần có mật khẩu + Dán địa chỉ máy tính + Dán đường dẫn sao lưu + Mật khẩu để hiển thị + Cuộc gọi chờ + Mã truy cập không đổi! + Mã truy cập đã được đặt! + bộ đếm PING + UI tiếng Ba Tư + 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. + Xin vui lòng kiểm tra rằng đường dẫn SimpleX là chính xác. + 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. + 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. + Xin vui lòng nhập đúng mật khẩu hiện tại. + Mở từ danh sách cuộc trò chuyện. + Cuộc gọi hình trong hình + Xin vui lòng báo cáo với các nhà phát triển: \n%s + Thông báo định kỳ đã bị tắt! + 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. + 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. + 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! + Quyền truy cập bị tự chối! + 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. + khoảng PING + 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. + Xin vui lòng liên lạc với quản trị viên nhóm. + Xin vui lòng báo cáo với các nhà phát triển. + 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. + Có lẽ vân tay chứng chỉ trong địa chỉ máy chủ là không chính xác + Đang chuẩn bị tải lên + Cổng + cổng %d + 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ó. + Xin vui lòng chờ trong khi tệp đang được tải từ thiết bị được liên kết + Địa chỉ máy chủ cài sẵn + Lưu lại bản nháp tin nhắn cuối cùng, với các tệp đính kèm. + Đang chuẩn bị tải xuống + 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. + Máy chủ cài sẵn + Xin vui lòng thử lại sau. + Xem trước + 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ó. + Giao diện tiếng Ba Lan + Xin vui lòng khởi động lại ứng dụng. + Các máy chủ đã kết nối trước đó + Định hình lại sự riêng tư + Quyền riêng tư & bảo mật + Bản cập nhật hồ sơ sẽ được gửi đến các liên hệ của bạn. + Cấm thả cảm xúc tin nhắn. + Cấm các cuộc gọi thoại/video. + Thông báo riêng tư + Các ảnh đại diện + Mật khẩu hồ sơ + Ghi chú riêng tư + Cấm xóa tin nhắn mà không thể phục hồi. + ĐỊNH TUYẾN TIN NHẮN RIÊNG TƯ + Tên hồ sơ: + ảnh đại diện + Hồ sơ và các kết nối máy chủ + chỗ để ảnh đại diện + Tên tệp riêng tư + Định tuyến tin nhắn riêng tư 🚀 + Lỗi định tuyến riêng tư + Ghi chú riêng tư + Định tuyến riêng tư + Chủ đề hồ sơ + Cấm thả cảm xúc tin nhắn. + Thời gian chờ giao thức trên mỗi KB + Bảo vệ các hồ sơ trò chuyện của bạn bằng mật khẩu! + 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ủ* + Bảo vệ địa chỉ IP + Cấm gửi tin nhắn trực tiếp tới các thành viên. + Cấm gửi tin nhắn thoại. + Thời gian chờ giao thức + Cấm gửi tin nhắn tự xóa. + Bảo vệ màn hình ứng dụng + Cấm gửi tệp và đa phương tiện. + Cấm gửi tin nhắn thoại. + Cấm gửi tin nhắn tự xóa. + Cấm gửi đường dẫn SimpleX + Được proxy + Địa chỉ hay đường dẫn dùng 1 lần? + với chỉ một liên hệ - chia sẻ trực tiếp hoặc thông qua bất kỳ ứng dụng tin nhắn nào.]]> + Cài đặt máy chủ .onion thành Không nếu proxy SOCKS không hỗ trợ chúng.]]> + %s.]]> + %s.]]> + %s.]]> + Đã thêm các máy chủ truyền tin nhắn + Thanh công cụ ứng dụng + Làm mờ + Chấp nhận điều kiện + Đã thêm các máy chủ truyền tệp & đa phương tiện + Đã chấp nhận điều kiện + Cài đặt địa chỉ + %s.]]> + %s.]]> + Kho lưu trữ GitHub của chúng tôi.]]> + %s.]]> + %s.]]> + %s.]]> + Hướng dẫn người dùng.]]> + %1$s rồi.]]> + Mức sử dụng pin ứng dụng / Không hạn chế trong phần cài đặt ứng dụng.]]> + %s]]> + Chọn cho phép trong hộp thoại tiếp theo để nhận thông báo ngay lập tức.]]> + %s, vui lòng chấp nhận điều kiện sử dụng.]]> + SimpleX chạy trong nền thay vì dùng thông báo đẩy.]]> + Mức sử dụng pin ứng dụng / Không hạn chế trong phần cài đặt ứng dụng.]]> + %1$s rồi.]]> + %1$s!]]> + %1$s rồi.]]> + Mở trong ứng dụng di động.]]> + Các bên vận hành máy chủ + 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.]]> + không được sử dụng cùng một cơ sở dữ liệu trên hai thiết .]]> + Lỗi chấp nhận điều kiện + Lỗi lưu máy chủ + Lỗi trong cấu hình máy chủ. + Bảo mật kết nối + Tạo đường dẫn dùng một lần + 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. + Tiếp tục + 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: + Các điều kiện sử dụng + 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. + Các điều kiện sẽ được chấp nhận vào: %s. + Lỗi thêm máy chủ + Lỗi cập nhật máy chủ + Các điều kiện đã được chấp nhận vào: %s. + 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. + Thiết bị Xiaomi: 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.]]> + Ứng dụng luôn chạy dưới nền + 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! + Cuộc trò chuyện sẽ bị xóa cho bạn - việc này không thể được hoàn tác! + Các cuộc trò chuyện công việc + Thêm các thành viên nhóm + Cuộc trò chuyện + %1$s rồi.]]> + Cuộc trò chuyện đã tồn tại! + Thêm bạn bè + đã chấp nhận lời mời + mã hóa đầu cuối, với bảo mật sau ượng tử trong các tin nhắn trực tiếp.]]> + Giới thiệu về các nhà cung cấp + Thêm các thành viên nhóm của bạn vào các cuộc trò chuyện. + Địa chỉ doanh nghiệp + Lỗi lưu cơ sở dữ liệu + 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. + Dành cho hồ sơ trò chuyện %s: + Kiểm tra tin nhắn mỗi 10 phút. + 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. + Cách nó cải thiện sự riêng tư + Mời để trò chuyện + Rời cuộc trò chuyện + Cho định tuyến riêng tư + để bảo mật siêu dữ liệu tốt hơn. + Đã cải thiện điều hướng trò chuyện + Xóa cuộc trò chuyện + Cho mạng xã hội + Rời cuộc trò chuyện? + Kết nối yêu cầu thiết lập lại mã hóa. + Đang trong quá trình thiết lập lại mã hóa. + Sửa + Sửa kết nối? + Xóa cuộc trò chuyện? + Tin nhắn trực tiếp giữa các thành viên bị cấm. + Bật logs + a + b + Kết nối chưa sẵn sàng. + Lỗi cập nhật danh sách trò chuyện + Lỗi tạo danh sách trò chuyện + Lỗi tải các danh sách trò chuyện + Các liên hệ + Nhóm + Thêm danh sách + Tất cả + Xóa + Xóa danh sách? + Chỉnh sửa + Tên danh sách... + Xóa hồ sơ trò chuyện cho + Tạo danh sách + Thêm vào danh sách + Các doanh nghiệp + 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 + Ưa thích + Danh sách + Xóa tin nhắn sau + Không có máy chủ tin nhắn. + Không có máy chủ để định tuyến tin nhắn riêng tư. + Không có máy chủ để nhận tin nhắn. + Tin nhắn quá lớn! + Các nhà cung cấp mạng + Thông báo và pin + Chỉ có các chủ cuộc trò chuyện mới có thể thay đổi các tùy chọn. + 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! + Nhà cung cấp mạng + Phi tập trung hóa mạng lưới + Không có tin nhắn + Không có cuộc trò chuyện nào + Không tìm thấy cuộc trò chuyện nào + Không có cuộc trò chuyện nào trong dánh sách %s. + Không có cuộc trò chuyện nào chưa được đọc + 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. + Không có máy chủ để nhận tệp. + Không có máy chủ để gửi tệp. + Không có dịch vụ nền + Máy chủ mới + Không có máy chủ tệp và đa phương tiện. + Thanh công cụ trò chuyện trong tầm + Ngẫu nhiên + Các máy chủ cài sẵn + Xem thay đổi + Xem điều kiện + Bên vận hành máy chủ + - 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. + Hoặc nhập tệp lưu trữ + Mã hóa kháng lượng tử + Sự riêng tư cho các khách hàng của bạn. + Xin vui lòng giảm kích thước tin nhắn và gửi lại. + 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. + Xác thực proxy + Mã QR + Mở bằng %s + Thanh công cụ trò chuyện trong tầm + Đánh giá ứng dụng + 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. + Các máy chủ được proxy + Đọc thêm + Thanh công cụ ứng dụng trong tầm + Hoặc chia sẻ một cách riêng tư + Bên vận hành + mã hóa đầu cuối kháng lượng tử + đã nhận lời đáp… + Đã nhận tin nhắn + Đang nhận qua + đã nhận, bị cấm + Lịch sử gần đây và đã cải thiện bot thư mục. + Việc nhận tệp vẫn chưa được hỗ trợ + Đã nhận lời đáp + Đị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. + đã nhận lời xác nhận… + 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. + Kết nối lại tất cả các máy chủ + Đã nhận tổng số + Nhận được lỗi + 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. + 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. + Đã nhận tin nhắn + Việc nhận tệp sẽ bị dừng lại + Đang nhận tin nhắn… + Đã nhận tin nhắn + Kết nối lại máy chủ? + Chỉ báo đã nhận bị tắt + Kết nối lại các máy chủ? + Người nhận không thể xem tin nhắn này đến từ ai. + Kết nối lại + Đang nhận đồng thời + Đã nhận vào + Đã nhận vào: %s + Xóa ảnh + Đã từ chối cuộc gọi + Nhắc lại sau + Làm mới + Đã giảm mức sử dụng pin + Từ chối + Các thiết bị di động từ xa + Xóa + Xóa + Xóa thành viên + Xóa thành viên + Xóa thành viên? + Từ chối + đã xóa + Bản ghi được cập nhật vào + Ghi lại tin nhắn thoại + Xóa mật khẩu khỏi cài đặt? + Xóa mật khẩu khỏi Keystore? + Bản ghi được cập nhật vào: %s + Xóa kho lưu trữ? + đã xóa địa chỉ liên lạc + đã xóa ảnh đại diện hồ sơ + 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. + đã xóa %1$s + đã xóa bạn + đã từ chối cuộc gọi + 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. + Đặt lại các màu + Lặp lại yêu cầu kết nối? + Đặt lại màu + Đặt lại về mặc định + Đặt lại + Lặp lại + đã yêu cầu kết nối + Tải lên lại lần nữa + Thiết lập lại mã hóa? + Nhập lại + Bắt buộc + Tải xuống lại lần nữa + Thiết lập lại + Đặt lại + Trả lời + Đặt lại về chủ đề người dùng + Thiết lập lại mã hóa + Khởi động lại + Khởi động lại ứng dụng để sử dụng cơ sở dữ liệu trò chuyện đã được nhập. + Lặp lại yêu cầu tham gia? + Đặt lại tất cả gợi ý + Đặt lại trở về chủ đề ứng dụng + Khởi động lại ứng dụng để tạo một hồ sơ trò chuyện mới. + Đặt lại tất cả số liệu thống kê + Đặt lại tất cả số liệu thống kê? + Lưu mật khẩu và mở kết nối trò chuyện + Gửi + KHỞI CHẠY KẾT NỐI TRÒ CHUYỆN + Lưu + Quét / Dán đường dẫn + Quét mã QR máy chủ + Lỗi khôi phục cơ sở dữ liệu + %s (hiện tại) + %s vào %s + Chọn các liên hệ + bí mật + gửi tệp chưa được hỗ trợ + Gửi tin nhắn tự xóa + 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. + Quét mã QR + đã lưu + đã lưu từ %s + Đã lưu từ + Quét mã QR từ máy tính + Đã được bảo mật + Thanh tìm kiếm chấp nhận đường dẫn lời mời. + Rào hàng đợi + Gửi lỗi + Lưu + Chọn + Mã truy cập tự hủy + Khôi phục + %s và %s đã được kết nối + %s đã được tải xuống + Khôi phục bản sao lưu cơ sở dữ liệu + Chạy khi sử dụng ứng dụng + Thu hồi tệp + Thu hồi tệp? + Thu hồi + gửi tin nhắn trực tiếp + Đã chọn %d + Gửi tin nhắn trực tiếp để kết nối + Đang lưu %1$s tin nhắn + (quét hoặc dán từ bảng nháp) + Chọn các bên vận hành mạng lưới để sử dụng. + Gửi tin nhắn trực tiếp + Lưu lời chào? + gửi thất bại + Quét mã bảo mật từ ứng dụng của liên hệ bạn. + GỬI CHỈ BÁO ĐÃ NHẬN TỚI + Tìm kiếm + Tìm kiếm hoặc dán đường dẫn SimpleX + Lưu danh sách + Thử lại + Lưu và thông báo tới liên hệ + Xem xét lại sau + mã bảo mật đã thay đổi + Lưu hồ sơ nhóm + Lưu và kết nối lại + Tính năng gửi chỉ báo đã nhận sẽ được bật cho tất cả liên hệ. + Xem xét lại các điều kiện + Tỷ lệ + Gửi + Chọn hồ sơ trò chuyện + Quét mã + Mã bảo mật + Lưu các máy chủ? + %s đã được kết nối + Đã lưu tin nhắn + giây + 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õ + Người gửi đã hủy quá trình truyền tệp. + Mã truy cập tự hủy + Các máy chủ WebRTC ICE đã được lưu sẽ bị xóa. + Khởi động lại kết nối trò chuyện + Lưu + 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. + Tiết lộ + Lưu và thông báo tới các liên hệ + Lưu vào thông báo tới các thành viên nhóm + Lưu mật khẩu hồ sơ + Thẩm định bảo mật + Nhận tệp một cách an toàn + Lưu và cập nhật hồ sơ nhóm + giây + Đã lưu + Tự hủy + Chọn + Mã truy cập tự hủy đã được kích hoạt! + Lưu mật khẩu trong Keystore + %s và %s + Chức vụ + Các nhóm trở nên an toàn hơn + Quét từ di động + Ghi chú + tìm kiếm + Thay đổi danh sách + Thay đổi thứ tự + Lưu các máy chủ + Lưu cài đặt tự động chấp nhận + Lưu tùy chọn? + Lưu cài đặt? + Khôi phục bản sao lưu cơ sở dữ liệu? + Lưu mật khẩu trong cài đặt + Mã truy cập tự hủy đã được đổi! + Người gửi có thể đã xóa yêu cầu kết nối. + Phiên bản máy chủ không tương thích với ứng dụng của bạn: %1$s. + Đặt 1 ngày + đặt ảnh đại diện mới + Các tin nhắn đã gửi sẽ bị xóa sau thời gian đã cài. + thông tin hàng đợi máy chủ: %1$s\n\ntin nhắn được nhận cuối cùng: %2$s + CÀI ĐẶT + Đã gửi vào + Địa chỉ máy chủ + Mã phiên + Lời đáp đã gửi + Đặt tên liên hệ… + Gửi tin nhắn động + Đặt tên liên hệ + Bên vận hành máy chủ đã được đổi. + Giao thức máy chủ đã được đổi. + 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ư. + Gửi xem trước đường dẫn + Các tin nhắn đã gửi + Thông tin các máy chủ + Máy chủ yêu cầu xác thực để tạo hàng đợi, kiểm tra mật khẩu + Việc gửi tệp sẽ bị dừng lại. + Gửi thư điện tử cho chúng tôi + Đã gửi vào: %s + Máy chủ đã được thêm cho bên vận hành %s. + Địa chỉ máy chủ không tương thích với cài đặt mạng. + Phiên bản máy chủ không tương thích với cài đặt mạng. + đã gửi + Cài đặt + Gửi các câu hỏi và ý tưởng + Đặt nó thay vì sử dụng xác thực hệ thống. + Đã gửi tin nhắn + Đặt mật khẩu để xuất + Đang gửi qua + Đặt mật khẩu + Cài đặt + Địa chỉ máy chủ không tương thích với cài đặt mạng: %1$s. + CÁC MÁY CHỦ + Máy chủ yêu cầu xác thực để tải lên, kiểm tra mật khẩu + Máy chủ + Đặt mã truy cập + Gửi chỉ báo đã nhận bị tắt cho %d liên hệ + Đặt tùy chọn nhóm + 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! + Gửi tin nhắn để cho phép gọi điện. + Đã gửi qua proxy + Gửi chỉ báo đã nhận được bật cho %d liên hệ + Đặt mật khẩu cơ sở dữ liệu + Gửi chỉ báo đã nhận + đặt địa chỉ liên lạc mới + Tin nhắn đã gửi + Đặt chủ đề mặc định + Gửi tối đa 100 tin nhắn cuối cùng tới các thành viên mới. + Tổng số đã gửi + Gửi Tin nhắn + Kiểm tra máy chủ thất bại! + 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ư. + Gửi chỉ báo đã nhận bị tắt cho %d nhóm + Gửi chỉ báo đã nhận được bật cho %d nhóm + Đã gửi trực tiếp + Hiển thị thông điệp tới các thành viên mới! + Chia sẻ địa chỉ SimpleX trên mạng xã hội. + Hiển thị liên hệ và tin nhắn + Hiển thị trạng thái tin nhắn + Hiển thị: + Chia sẻ địa chỉ một cách công khai + Hiển thị + Hiển thị các tin nhắn cuối cùng + Chia sẻ + Hiển thị bảng điều khiển trong cửa sổ mới + Lỗi tạo báo cáo + Chia sẻ tin nhắn… + Hiển thị danh sách trò chuyện trong cửa sổ mới + Chỉ hiển thị liên hệ + Chia sẻ đường dẫn dùng 1 lần này + Chia sẻ với các liên hệ + Chia sẻ tệp… + Chia sẻ địa chỉ + Chia sẻ đa phương tiện… + Chia sẻ hồ sơ + Hiển thị tùy chọn cho nhà phát triển + Tạo khuôn ảnh đại diện + Thiết lập mật khẩu cơ sở dữ liệu + Đang hiển thị thông tin cho + Chia sẻ đường dẫn dùng 1 lần + Chia sẻ địa chỉ với các liên hệ? + Hiển thị tỷ lệ phần trăm + Hiển thị lỗi nội bộ + Chia sẻ đường dẫn + Chia sẻ đường dẫn dùng 1 lần với một người bạn + Lỗi lưu cài đặt + Hiển thị bản xem trước + Các cuộc gọi SimpleX Chat + simplexmq: v%s (%2s) + Các đường dẫn SimpleX là không được phép + Khóa SimpleX + Các đường dẫn SimpleX + Các tin nhắn SimpleX Chat + Địa chỉ SimpleX + 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. + Chế độ Khóa SimpleX + Địa chỉ liên lạc SimpleX Chat + Đường dẫn nhóm SimpleX + Các đường dẫn SimpleX + Lời mời SimpleX dùng một lần + Khóa SimpleX + 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. + SimpleX + Địa chỉ SimpleX + Tắt? + Tắt + Sự an toàn của SimpleX Chat đã được kiểm định bởi Trail of Bits. + Dịch vụ SimpleX Chat + Khóa SimpleX đã được bật + Hiển thị mã QR + Đị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. + Logo SimpleX + Hiển thị các lượt yêu cầu API chậm + Địa chỉ SimpleX hay đường dẫn dùng 1 lần? + Khóa SimpleX không được bật! + Các đường dẫn SimpleX là bị cấm. + Chỉ người gửi và các kiểm duyệt mới xem được + Lưu trữ bản báo cáo? + đã lưu trữ bản báo cáo + 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. + Lưu trữ + Một lý do khác + kiểm duyệt viên + 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. + Đội ngũ SimpleX + Các giao thức SimpleX đã được xem xét bởi Trail of Bits. + Đã đơn giản hóa chế độ ẩn danh + Vi phạm các nguyên tắc cộng động + Nội dung không phù hợp + Hồ sơ không phù hợp + Chỉ bạn và các kiểm duyệt viên mới xem được + Báo cáo + Lý do báo cáo? + Báo cáo khác: sẽ chỉ có các kiểm duyệt viên trong nhóm là xem được. + 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. + Báo cáo vi phạm: sẽ chỉ có các kiểm duyệt viên trong nhóm là xem được. + Kích thước + %s đã được xác minh + %s chưa được xác minh + Các tin nhắn bị bỏ qua + Bỏ qua việc mời các thành viên + Loa ngoài bật + Âm thanh đã bị tắt + Ổn định + PROXY SOCKS + Các nhóm nhỏ (tối đa 20 thành viên) + Một vài lỗi không nghiêm trọng đã xảy ra trong lúc nhập: + Loa ngoài tắt + Lưu trữ báo cáo + Xóa báo cáo + Các máy chủ %s + Hàm chạy chậm + Nhẹ + %s giây + Một vài máy chủ không vượt qua bài kiểm tra: + Thả sao trên Github + %s, %s và %s đã được kết nối + Máy chủ SMP + %s, %s và %d thành viên khác đã được kết nối + mã hóa đầu cuối tiêu chuẩn + Hình vuông, hình tròn, hoặc bất kỳ hình thù gì ở giữa. + proxy SOCKS + Cài đặt proxy SOCKS + đang bắt đầu… + %s, %s và %d thành viên + %s: %s + Bỏ qua phiên bản này + Một số tệp đã không được xuất + Loa ngoài + Bắt đầu kết nối trò chuyện? + Các máy chủ SMP + Bắt đầu kết nối trò chuyện + Tin nhắn rác + Mạnh + Bắt đầu từ %s. + Dừng + Dừng kết nối trò chuyện? + %s đã được tải lên + Đã đăng ký + đã lưu trữ báo cáo bởi %s + Dừng tệp + 1 báo cáo + %d báo cáo + Các báo cáo của thành viên + Lỗi đăng ký dài hạn + Các báo cáo + 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. + Hỗ trợ bluetooth và nhiều cải tiến khác. + Các đăng ký dài hạn bị bỏ qua + Dừng nhận tệp? + Dừng gửi tệp? + Tạo cuộc trò chuyện mới + Số liệu thống kê + Bắt đầu định kỳ + Dừng + Dừng chia sẻ địa chỉ? + 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. + Nhập + Đang dừng kết nối trò chuyện + Dừng chia sẻ + gạch ngang + Dừng kết nối trò chuyện + Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate! + Hệ thống + Nhấn để tham gia + Thời gian chờ kết nối TCP + Nhấn để kích hoạt hồ sơ. + Hệ thống + Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate! + Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate! + Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate! + Chế độ hệ thống + HỖ TRỢ SIMPLEX CHAT + Lỗi tệp tạm thời + Nhấn nút + Kết nối TCP + Hệ thống + Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate! + Nhấn để Kết nối + Nhấn Tạo địa chỉ SimpleX ở menu để tạo sau. + Nhấn để tham gia một cách ẩn danh + Hệ thống + Chuyển đổi âm thanh và video trong lúc gọi. + Chuyển đổi hồ sơ trò chuyện cho các lời mời dùng 1 lần. + Kiểm tra thất bại tại bước %s. + Xác thực hệ thống + Nhấn để quét + Cảm ơn bạn đã cài đặt SimpleX Chat! + Kiểm tra máy chủ + Kiểm tra các máy chủ + Nhấn để bắt đầu một cuộc trò chuyện mới + Nhấn để dán đường dẫn + Đuôi + Chuyển đổi + 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. + Tương lai của nhắn tin + 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. + Các tin nhắn sẽ bị xóa cho tất cả các thành viên. + 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! + Ứ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). + Kết nối đã bị chặn + Kết nối đã bị chặn bởi bên vận hành máy chủ:\n%1$s. + Nội dung vi phạm các điều kiện sử dụng + Tin nhắn rác + Tệp đã bị chặn bởi bên vận hành máy chủ:\n%1$s. + Cơ sở dữ liệu đang không hoạt động như bình thường. Nhấn để tìm hiểu thêm + Không + Mở đường dẫn + Yêu cầu + Mở đường dẫn web? + Mở các đường dẫn từ danh sách trò chuyện + Kết nối bạn đã chấp nhận sẽ bị hủy bỏ! + Liên hệ mà bạn đã chia sẻ đường dẫn này sẽ không thể kết nối! + 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. + Ứ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. + Ứng dụng có thể bị đóng sau 1 phút chạy trong nền. + Ứ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. + Mã mà bạn đã quét không phải là một mã QR dẫn SimpleX. + Chủ đề + Mã băm của tin nhắn trước có sự khác biệt.\" + 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. + Nỗ lực đổi mật khẩu cơ sở dữ liệu đã không được hoàn thành. + Tên thiết bị sẽ được chia sẻ với thiết bị di động đã được kết nối. + CÁC CHỦ ĐỀ + 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. + Hồ sơ chỉ được chia sẻ với các liên hệ của bạn. + 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ử. + Mật khẩu được lưu trữ trong cài đặt dưới dạng thuần văn bản. + Thiết bị này + 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. + Tin nhắn sẽ bị đánh dấu là đã được kiểm duyệt cho tất cả các thành viên. + Tin nhắ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 - 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. + 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. + Nhóm này không còn tồn tại. + Nhóm này có trên %1$d thành viên, chỉ báo đã nhận không được gửi. + Các tin nhắn sẽ bị đánh dấu là đã được kiểm duyệt cho tất cả thành viên. + Tên thiết bị này + Chúng có thể bị ghi đề trong cài đặt liên hệ và nhóm. + Cuộc trò chuyện này được bởi vệ bằng mã hóa đầu cuối. + Tính năng này chưa được hỗ trợ. Hãy thử bản phát hành tiếp theo. + Dấu tick thứ hai mà chúng ta từng thiếu! ✅ + 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 + 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. + Bên vận hành được cài sẵn thứ hai trong ứng dụng! + 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. + Văn bản bạn vừa dán không phải là một đường dẫn SimpleX. + 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. + 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. + Các cài đặt này là cho hồ sơ trò chuyện hiện tại của bạn + 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. + 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ủ. + 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. + 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 + Báo cáo sẽ được lưu trữ cho bạn. + Tin nhắn này đã bị xóa hoặc vẫn chưa được nhận. + Mã QR này không phải là một đường dẫn! + Đường dẫn này không phải là một đường dẫn kết nối hợp lệ! + Chuyển đổi danh sách trò chuyện: + Thời gian chờ đã hết trong khi kết nối tới máy tính + Để cho phép một ứng dụng di động kết nối tới máy tính, mở cổng này trong tường lửa của bạn, nếu bạn có bật nó lên + Để bảo vệ sự riêng tư của bạn, SimpleX sử dụng các ID riêng biệt cho mỗi liên hệ bạn có. + Để nhận thông báo, xin vui lòng nhập mật khẩu cơ sở dữ liệu + Quá nhiều ảnh! + Quá nhiều video! + Để gửi + Đườ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. + Để 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. + Chuỗi ký tự này không phải là một đường dẫn kết nối! + Để 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. + Để nhận + Để 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. + Tiêu đề + Chuyển đổi ẩn danh khi kết nối. + Để bảo vệ múi giờ, các tệp hình ảnh/âm thanh sử dụng UTC. + Văn bản này có sẵn trong cài đặt + Để đượ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. + Để ẩn các tin nhắn không mong muốn. + Để 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. + Đây là đường dẫn dùng một lần của riêng bạn! + 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 + Đây là địa chỉ SimpleX của riêng bạn! + Để kết nối qua đường dẫn + Để 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. + Để 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. + Cách ly truyền tải + Các máy chủ không xác định + trạng thái không xác định + Lỗi không xác định + Các tin nhắn không được gửi đi + Bỏ yêu thích + Bỏ chặn cho tất cả + Bỏ chặn thành viên cho tất cả? + Để bắt đầu một cuộc trò chuyện mới + Bỏ ẩn hồ sơ trò chuyện + Bỏ ẩn hồ sơ + đã bỏ chặn %s + Bỏ chặn + Bỏ chặn thành viên + Bỏ chặn thành viên? + Độ trong suốt + Các phiên truyền tải + Các máy chủ không xác định! + gửi mà không được cho phép + (để chia sẻ với liên hệ của bạn) + Bỏ ẩn + Bật + Tổng + không xác định + Đ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). + Đ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. + Để 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. + Cách ly truyền tải + định dạng tin nhắn không xác định + Lỗi cơ sở dữ liệu không xác định: %s + 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. + Sử dụng các thông tin đăng nhập proxy khác cho mỗi kết nối. + Sử dụng các thông tin đăng nhập proxy khác nhau cho mỗi hồ sơ. + Cập nhật chế độ cách ly truyền tải? + Hủy liên kết máy tính? + Tải lên tệp + Có bản cập nhật: %s + 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ủ. + Đã tải lên các tệp + chưa đọc + Cập nhật + Cập nhật mật khẩu cơ sở dữ liệu + Hủy liên kết + Cập nhật cài đặt mạng? + Tải lên đã thất bại + Đang tải lên bản lưu trữ + Tải xuống bản cập nhật đã bị hủy + Bật thông báo + Nâng cấp và mở kết nối trò chuyện + Bật thông báo + đã cập nhật hồ sơ nhóm + đã cập nhật hồ sơ + Sử dụng hồ sơ hiện tại + Sử dụng kết nối Internet trực tiếp? + Không được bảo vệ + 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. + Lỗi tải lên + Nâng cấp ứng dụng một cách tự động + Đã tải lên + Cập nhật + Cập nhật + Mở khóa + Xác minh bảo mật kết nối + Sử dụng cho các tệp + Dùng ứng dụng với một tay. + Xác minh mã trên di động + Xác minh mật khẩu cơ sở dữ liệu + Sử dụng định tuyến riêng tư với các máy chủ không xác định. + Sử dụng %s + Sử dụng các máy chủ + Dùng ứng dụng khi đang trong cuộc gọi. + Xác minh kết nối + Đang sử dụng các máy chủ SimpleX Chat. + Xác minh các kết nối + Sử dụng cho các tin nhắn + Tên người dùng + Sử dụng các thông tin đăng nhập ngẫu nhiên + Sử dụng các dịch vụ .onion + Xác minh mã bảo mật + Sử dụng hồ sơ ẩn danh mới + qua %1$s + Sử dụng proxy SOCKS + Sử dụng SimpleX Chat + Sử dụng cho các kết nối mới + Sử dụng máy chủ + Sử dụng proxy SOCKS? + Sử dụng các máy chủ SimpleX Chat? + Xác minh mật khẩu + Xác minh mã với máy tính + Sử dụng mật khẩu ngẫu nhiên + Sử dụng từ máy tính + 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ệ. + Xem các điều kiện + qua đường dẫn dùng một lần + Các tin nhắn thoại bị cấm. + Lịch sử hữu hình + Cuộc gọi video + Video đã được gửi + Tin nhắn thoại… + Các tin nhắn thoại là không được cho phép + Tin nhắn thoại + Video + Tin nhắn thoại (%1$s) + Video bật + Các tin nhắn thoại bị cấm trong cuộc trò chuyện này. + Tin nhắn thoại + Qua giao thức kháng lượng tử an toàn. + qua đường dẫn nhóm + Qua trình duyệt + video + Xem các điều kiện đã được cập nhật + Xem sự cố + Video tắt + Video sẽ được nhận khi liên hệ của bạn hoàn thành việc tải nó lên. + 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! + Xem mã bảo mật + Các tệp và video với kích thước tối đa lên tới 1gb + cuộc gọi video + thông qua relay + Tin nhắn thoại + cuộc gọi video (không được mã hóa đầu cuối) + qua đường dẫn địa chỉ liên lạc + Video + 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ủ. + Website + Xin chào %1$s! + Khi có sẵn + Các tin nhắn thoại bị cấm! + Lời chào + Màu sơ cấp hình nền + Đang chờ hình ảnh + Đang chờ hình ảnh + đang chờ xác nhận… + Có gì mới + tuần + Lời chào + Màu nền hình nền + Đang chờ máy tính… + Đang chờ di động để kết nối: + Xin chào! + Đang chờ video + muốn kết nối với bạn! + Đang chờ tệp + đang chờ trả lời… + Lời chào quá dài + - 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ử. + Cảnh báo: bạn có thể mất một số dữ liệu! + 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 + Các máy chủ ICE WebRTC + Khi ứng dụng đang chạy + Khi kết nối các cuộc gọi video và âm thanh. + Khi IP bị ẩn + Đang chờ video + + Thay đổi xóa tin nhắn tự động? + Các tin nhắn trong cuộc trò chuyện này sẽ không bao giờ bị xóa. + Máy chủ XFTP + 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. + Sẽ được kích hoạt trong các cuộc trò chuyện trực tiếp! + Mạng ethernet có dây + 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. + Mật khẩu sai rồi! + WiFi + + Mật khẩu cơ sở dữ liệu sai + 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. + mặc định (%s) + Xóa tin nhắn trò chuyện khỏi thiết bị của bạn. + Tắt xóa tin nhắn tự động? + Tắt tính năng xóa tin nhắn + Đặt tên cuộc trò chuyện… + 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. + Các máy chủ XFTP + 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. + Với các tệp và đa phương tiện được mã hóa. + Mức sử dụng pin đã được giảm xuống. + Mức sử dụng pin đã được giảm xuống. + Với lời chào tùy chọn. + 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ó. + 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. + 1 năm + 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. + + 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. + Bạn có thể tạo nó sau + Bạn có thể thay đổi nói trong cài đặt Giao diện. + Bạn đang tham gia nhóm thông qua đường dẫn này. + Bạn có thể bật vào lúc sau thông qua Cài đặt + BẠN + 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. + Bạn có thể thử một lần nữa. + Bạn đã kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này. + 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. + Bạn có thể gửi tin nhắn tới %1$s từ Các liên hệ được lưu trữ. + Bạn đã kết nối với %1$s rồi. + Bạn cho phép + 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. + 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ủ. + Bạn đang kết nối thông qua đường dẫn dùng một lần này! + Bạn có thể thử một lần nữa. + 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. + Bạn được mời vào nhóm + bạn: %1$s + Bạn có thể tùy chỉnh các máy chủ thông qua cài đặt. + Bạn có thể đặt tên kết nối, để nhớ xem đường dẫn đã được chia sẻ với ai. + bạn là quan sát viên + Bạn có thể sao chép và giảm kích thước tin nhắn để gửi nó đi. + 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. + 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. + bạn đã chặn %s + Bạn đã chấp nhận kết nối + bạn + bạn được mời vào nhóm + Các cuộc gọi của bạn + Bạn có thể bật Khóa SimpleX thông qua Cài đặt. + Bạn không có cuộc trò chuyện nào + 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. + Bạn đã tham gia nhóm này + Bạn đã tham gia nhóm này. Đang kết nối tới thành viên nhóm đi mời. + Bạn đã không thể được xác minh; xin vui lòng thử lại. + bạn đã thay đổi chức vụ của mình thành %s + Bạn có thể lưu bản lưu trữ đã được xuất. + 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ó. + 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. + bạn đã rời + 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ọ. + bạn đã thay đổi chức vụ của %s thành %s + bạn đã thay đổi địa chỉ + Bạn đã yêu cầu kết nối thông qua địa chỉ này rồi! + Bạn đã mời một liên hệ + 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. + Bạn có thể xem đường dẫn mời lần nữa trong chi tiết kết nối. + Bạn kiểm soát cuộc trò chuyện của mình! + Bạn không thể gửi tin nhắn! + 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. + 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ọ. + Bạn có thể di chuyển cơ sở dữ liệu đã được xuất. + 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ệ. + bạn đã thay đổi địa chỉ cho %s + Bạn có thể sử dụng markdown để định dạng tin nhắn: + 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. + Bạn quyết định ai có thể kết nối tới. + Cơ sở dữ liệu trò chuyện của bạn + Sự riêng tư của bạn + 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. + bạn đã xóa %1$s + Các liên hệ của bạn vẫn sẽ được kết nối. + 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. + Các liên hệ của bạn có thể cho phép xóa tin nhắn hoàn toàn. + Thu phóng + Các máy chủ SMP của bạn + Bạn đã chia sẻ đường dẫn dùng một lần + Hồ sơ trò chuyện của bạn sẽ được gửi tới các thành viên nhóm + Hồ sơ hiện tại của bạn + 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 + Các tùy chọn của bạn + Hồ sơ ngẫu nhiên của bạn + Bạn đã gửi lời mời nhóm + Bạn đã từ chối lời mời nhóm + bạn đã bỏ chặn %s + Hồ sơ trò chuyện của bạn sẽ được gửi\ntới liên hệ của bạn + bạn đã chia sẻ đường dẫn ẩn danh dùng một lần + Địa chỉ SimpleX của bạn + 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). + Các máy chủ ICE của bạn + 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 + 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 + 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! + Hồ sơ %1$s sẽ được chia sẻ. + Các máy chủ XFTP của bạn + 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. + Bạn sẽ kết nối với tất cả các thành viên nhóm. + 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. + Các máy chủ của bạn + 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. + Yêu cầu kết nối sẽ được gửi tới thành viên nhóm này. + 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. + 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. + 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). + 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. + Các liên hệ của bạn + Các hồ sơ trò chuyện của bạn + Cài đặt của bạn + Máy chủ của bạn + Địa chỉ máy chủ của bạn + Các máy chủ ICE của bạn + Thông tin định danh của bạn có thể bị gửi mà không được mã hóa. + 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. + 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ơ. + 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! + 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! + 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. + Cổng TCP để nhắn tin + Sử dụng cổng TCP %1$s khi không có cổng nào được chỉ định. + Sử dụng cổng web + Bạn có thể nhắc đến tối đa là %1$s thành viên trong một tin nhắn! + Tắt thông báo tất cả + Những lời nhắc chưa đọc + 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. + Tất cả các báo cáo sẽ được lưu trữ cho bạn. + Lưu trữ tất cả báo cáo? + Lưu trữ %d báo cáo? + Lưu trữ các báo cáo + Cho tất cả các kiểm duyệt viên + Cho tôi + Báo cáo: %s + Cho phép báo cáo tin nhắn tới các kiểm duyệt viên. + Cấm báo cáo tin nhắn tới các kiểm duyệt viên. + Báo cáo tin nhắn bị cấm trong nhóm này. + Nhận thông báo khi được nhắc đến. + Giúp các quản trị viên quản lý các nhóm của họ. + Nhắc đến các thành viên 👋 + Sắp xếp các cuộc trò chuyện thành danh sách + Xóa nhóm nhanh hơn. + Gửi tin nhắn nhanh hơn. + Không còn bỏ lỡ các tin nhắn quan trọng nữa. + Bảo mật và riêng tư hơn + Nhóm hoạt động hiệu quả hơn + Gửi các báo cáo riêng tư + đã từ chối + đã từ chối + Thiết lập giờ hết hạn cho tin nhắn trong các cuộc trò chuyện. + Tên các tệp tin đa phương tiện riêng tư. + Lỗi đọc mật khẩu cơ sở dữ liệu + Tất cả các tin nhắn mới từ những thành viên này sẽ bị ẩn! + Chặn các thành viên cho tất cả? + các kiểm duyệt viên + Các tin nhắn từ những thành viên này sẽ được hiển thị! + 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! + 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! + 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. + đang chờ xử lý + Các điều kiện đã được cập nhật + 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. + đang chờ phê duyệt + Bỏ chặn các thành viên cho tất cả? + Xóa các thành viên? + Chính sách quyền riêng tư và các điều kiện sử dụng. + Bằng việc sử dụng SimpleX Chat, bạn đồng ý:\n- chỉ gửi nội dung hợp pháp trong các nhóm công khai.\n- tôn trọng những người dùng khác - không gửi tin rác. + Các cuộc trò chuyện riêng tư, nhóm và liên hệ của bạn không thể truy cập được đối với các bên vận hành máy chủ. + Chấp nhận + Định cấu hình các bên vận hành máy chủ + Đường dẫn này yêu cầu một phiên bản ứng dụng mới hơn. Vui lòng nâng cấp ứng dụng hoặc yêu cầu liên hệ của một gửi cho một đường dẫn tương thích. + Đường dẫn kênh SimpleX + Đường dẫn kết nối không được hỗ trợ + Sử dụng đường dẫn ngắn (BETA) + Toàn bộ đường dẫn + Đường dẫn ngắn + Tắt + Các máy chủ cài sẵn + Chỉ sử dụng cổng TCP 443 cho các máy chủ cài sẵn. + Tất cả máy chủ + 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 536207892d..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 @@ a + b 1天 关于 SimpleX - 所有群组成员将保持连接。 + 所有群成员将保持连接。 关于 SimpleX Chat 以上,然后: 接受 @@ -15,14 +15,14 @@ 已接受通话 接受 通过在 %d 端口的 SOCKS 代理访问服务器?启用该选项前必须先启动代理。 - 添加服务器…… + 添加服务器 添加另一设备 管理员 扫描二维码来添加服务器。 高级网络设置 接受连接请求? 接受隐身聊天 - 管理员可以创建链接以加入群组。 + 管理员可以创建链接以加入群。 添加预设服务器 通过链接连接 已建立连接 @@ -39,7 +39,7 @@ 消息和文件 添加个人资料 所有聊天记录和消息将被删除——这一行为无法撤销! - 所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。 + 所有聊天记录和消息将被删除——这一行为无法撤销!只有你的消息会被删除。 允许发送语音消息。 允许语音消息? 删除 @@ -47,14 +47,13 @@ 删除 删除地址? 在此后删除 - 删除档案 已删除 删除文件和媒体文件? 为所有人删除 为我删除 为所有聊天资料删除文件 - 删除群组 - 删除群组? + 删除群 + 删除群? 删除链接 删除链接? 连接 @@ -64,35 +63,34 @@ 连接 通过一次性链接进行连接? 通过联系人地址进行连接? - 加入群组? - 通过群组链接/二维码连接 + 加入群? + 通过群链接/二维码连接 总是通过中继连接 - 允许您的联系人不不可逆地删除已发送消息。(24小时) + 允许你的联系人不不可逆地删除已发送消息。(24小时) 联系人允许 - 仅有您的联系人许可后才允许语音消息。 - 您: %1$s - 允许您的联系人发送语音消息。 + 允许语音消息,前提是你的联系人允许这样的消息。 + 你: %1$s + 允许你的联系人发送语音消息。 始终 始终开启 - 允许您的联系人发送限时消息。 + 允许你的联系人发送限时消息。 应用程序构建:%s 所有联系人会保持连接。 允许 允许向成员发送私信。 允许发送限时消息。 删除地址 - 删除聊天档案? 删除聊天资料? 删除联系人 删除联系人? - 已删除群组 + 已删除群 删除图片 - 仅有您的联系人许可后才允许限时消息。 - 只有您的联系人同意才允许不可逆地删除消息。(24小时) + 允许限时消息,前提是你的联系人允许这样的消息。 + 允许不可逆的消息删除,前提是你的联系人允许这样做。(24小时) 允许不可逆地删除已发送消息。(24小时) 为此删除聊天资料 删除数据库 - 在您重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。 + 在你重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。 安卓密钥库系统用来安全地保存密码——来确保通知服务运作。 外观 应用程序版本 @@ -109,24 +107,23 @@ 消息散列值错误 错误消息 ID 语音和视频通话 - 启用电池优化,关闭了后台服务和对新消息的定期请求。您可以在设置里重新启用它们。 + 启用电池优化,关闭了后台服务和对新消息的定期请求。你可以在设置里重新启用它们。 后台服务始终运行——一旦有消息,就会显示通知。 关闭音频 开启音频 已要求接收图片 - ,用于您在应用程序中的每个聊天资料 。]]> - 每个联系人和群组成员 将使用单独的 TCP 连接(和 SOCKS 凭证)。 -\n请注意:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。 + ,用于你在应用程序中的每个聊天资料 。]]> + 每个联系人和群成员。\n请注意:如果你有很多连接,你的电池和流量消耗可能会大大增加,并且某些连接可能会失败。]]> 返回 - 最长续航 。您只会在应用程序运行时收到通知(无后台服务)。]]> - 较长续航 。后台服务每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]> + 最长续航 。你只会在应用程序运行时收到通知(无后台服务)。]]> + 较长续航 。应用每 10 分钟检查一次消息。你可能会错过来电或者紧急信息。]]> 加粗 - 您和您的联系人都可以不可逆地删除已发送的消息。(24小时) - 您和您的联系人都可以发送限时消息。 - 您和您的联系人都可以发送语音消息。 + 你和你的联系人都可以不可逆地删除已发送的消息。(24小时) + 你和你的联系人都可以发送限时消息。 + 你和你的联系人都可以发送语音消息。 可以在设置里禁用它 - 应用程序运行时仍会显示通知。]]> - 使用更多电量 !后台服务始终运行——一旦收到消息,就会显示通知。]]> - 请注意:如果您丢失密码,您将无法恢复或者更改密码。]]> + 使用更多电量 !应用始终在后台运行——一即刻显示通知。]]> + 请注意:如果你丢失密码,你将无法恢复或者更改密码。]]> 通话已结束! 无法邀请联系人! 无法邀请联系人! @@ -136,7 +133,7 @@ 通话结束 更改数据库密码? 通话错误 - 为您更改地址 + 为你更改地址 通话中 通话进行中 呼叫中…… @@ -150,66 +147,64 @@ 无法接收文件 无法初始化数据库 将 %s 的角色更改为 %s - 将您的角色更改为 %s + 将你的角色更改为 %s 改变角色 - 更改群组角色? + 更改群角色? 取消链接预览 正在为 %s 更改地址…… 更改地址中…… 更改地址中…… - 创建您的资料 + 创建你的资料 聊天数据库已删除 聊天数据库已导入 钥匙串错误 - 聊天档案 - 聊天档案 聊天控制台 聊天数据库 聊天已停止 聊天运行中 聊天已停止 联系人偏好设置 - 您的偏好设置 - 群组偏好设置 - 只有群主可以改变群组偏好设置。 + 你的偏好设置 + 群偏好设置 + 只有群主可以改变群偏好设置。 保存偏好设置? - 设置群组偏好设置 + 设置群偏好设置 重新定义隐私 改进的隐私和安全 隐身聊天 - 加入群组中 + 加入群中 加入隐身聊天 隐身模式 点击开始一个新聊天 - 您的随机资料 + 你的随机资料 通过联系地址链接隐身 - 通过群组链接隐身 - 您分享了一次性链接隐身聊天 + 通过群链接隐身 + 你分享了一次性链接隐身聊天 点击以加入隐身聊天 - 您的聊天资料将被发送给群组成员 - 您正在尝试邀请与您共享隐身个人资料的联系人加入您使用主要个人资料的群组 - 隐身模式通过为每个联系人使用新的随机配置文件来保护您的隐私。 - 您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人 + 你的聊天资料将被发送给群成员 + 你正在尝试邀请与你共享隐身个人资料的联系人加入你使用主要个人资料的群 + 隐身模式通过为每个联系人使用新的随机配置文件来保护你的隐私。 + 你正在为该群使用隐身个人资料——为防止共享你的主要个人资料,不允许邀请联系人 通过一次性链接隐身 只有群主可以启用语音信息。 - 您的隐私设置 + 你的隐私设置 隐私和安全 保存服务器 它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。 - 当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。 + 当你与某人共享隐身聊天资料时,该资料将用于他们邀请你加入的群。 改进的服务器配置 电邮 编辑图片 - 编辑群组资料 + 编辑群资料 加密数据库错误 导出聊天数据库错误 导入聊天数据库错误 - 加入群组错误 + 加入群错误 删除用户资料错误 数据库密码不同于保存在密钥库中的密码。 数据库加密密码将被更新并存储在密钥库中。 数据库将被加密,密码存储在密钥库中。 - 在密匙库中没有找到密码,请手动输入。如果您使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。 + 在密匙库中没有找到密码,请手动输入。如果你使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。 从密钥库中删除密码? 在密钥库中保存密码 SimpleX Chat 服务 @@ -234,10 +229,10 @@ 按聊天资料(默认)或按连接(BETA)。 检查服务器地址并重试。 清除验证 - 关闭按键 + 关闭 配置 ICE 服务器 确认 - 确认您的证书 + 确认你的证书 已连接 已连接 连接 @@ -246,14 +241,14 @@ 连接中 每10分钟检查一次新消息,最长检查1分钟 已连接 - 与您的联系人比较安全码。 + 与你的联系人比较安全码。 语音通话来电 更改设置错误 - 群组邀请不再有效,已被发件人删除。 + 群邀请不再有效,已被发件人删除。 重复的显示名! 创建资料错误! 接受联系人请求错误 - 删除群组错误 + 删除群错误 删除待定的联系人连接错误 接收文件错误 切换资料错误! @@ -264,7 +259,7 @@ 对于每个人 解码错误 图片保存到相册 - 图片将在您的联系人在线时收到,请稍等或稍后查看! + 图片将在你的联系人在线时收到,请稍等或稍后查看! 保存文件错误 文件 未找到文件 @@ -301,17 +296,17 @@ 加密 加密数据库 输入正确密码。 - 不活跃群组 + 不活跃群 创建者 连接中(已接受) 连接中(已宣布) 扩展角色选择 - 群组链接 - 删除群组链接错误 + 群链接 + 删除群链接错误 数据库 ID 删除成员错误 更改角色错误 - 群组 + 限时消息 %d 天 连接中…… @@ -320,7 +315,7 @@ 联系人姓名 连接中(介绍邀请) 连接中…… - 联系人可以将信息标记为删除;您将可以查看这些信息。 + 联系人可以将信息标记为删除;你将可以查看这些信息。 贡献 已检查联系人 联系人已隐藏: @@ -328,12 +323,12 @@ 上下文图标 已复制到剪贴板 连接中…… - 创建群组链接 - 创建私密群组 + 创建群链接 + 创建私密群 创建链接 创建一次性邀请链接 创建队列 - 创建私密群组 + 创建私密群 不同的名字、头像和传输隔离。 法语界面 如何使用它 @@ -357,17 +352,17 @@ 全名: 输入你的名字: 已结束 - 群组已删除 - 将为所有成员删除群组——此操作无法撤消! + 群已删除 + 将为所有成员删除群——此操作无法撤消! 直接 私信 已启用 - 群组成员可以发送语音消息。 - 群组链接 + 成员可以发送语音消息。 + 群链接 启动聊天错误 数据库已加密! 加密数据库? - 数据库使用随机密码进行加密,您可以更改它。 + 数据库使用随机密码进行加密,你可以更改它。 打开聊天需要数据库密码。 停止聊天错误 导入聊天数据库? @@ -376,16 +371,16 @@ 输入密码…… 数据库加密密码将被更新。 数据库错误 - 群组资料已更新 - 群组全名: + 群资料已更新 + 群全名: 深色 已为联系人启用 - 为您启用 - 群组成员可以发送限时消息。 + 为你启用 + 成员可以发送限时消息。 创建个人资料 工作原理 - 未找到群组! - 创建群组链接错误 + 未找到群! + 创建群链接错误 连接中(已介绍) 数据库使用随机密码进行加密。请在导出前更改它。 数据库密码和导出 @@ -402,25 +397,25 @@ 删除联系人错误 更新网络配置错误 删除联系人请求错误 - 保存群组资料错误 + 保存群资料错误 保存 SMP 服务器错误 保存 ICE 服务器错误 发送消息错误 完整链接 - 输入群组名: - 群组邀请已过期 - 将为您删除群组——此操作无法撤消! - 群组资料存储在成员的设备上,而不是服务器上。 - 如果您选择拒绝发件人,将不会收到通知。 - 如何使用您的服务器 - 如果您确认,消息服务器将能够看到您的 IP 地址和您的提供商——以及您正在连接的服务器。 + 输入群名: + 群邀请已过期 + 将为你删除群——此操作无法撤消! + 群资料存储在成员的设备上,而不是服务器上。 + 如果你选择拒绝发件人,将不会收到通知。 + 如何使用你的服务器 + 如果你确认,消息服务器将能够看到你的 IP 地址和你的提供商——以及你正在连接的服务器。 图片 图片已发送 设备验证被禁用。关闭 SimpleX 锁定。 - 没有启用设备验证。一旦启用设备验证,您可以通过设置打开 SimpleX 锁定。 + 没有启用设备验证。一旦启用设备验证,你可以通过设置打开 SimpleX 锁定。 禁用 SimpleX 锁定 目前支持的最大文件尺寸是 %1$s。 - 文件将在您的联系人在线时收到,请稍等或稍后再查看! + 文件将在你的联系人在线时收到,请稍等或稍后再查看! 从图库 照片 视频 @@ -428,10 +423,10 @@ 翻转相机 启用自动删除消息? 用于控制台 - 此群中禁止成员之间私信。 - 该组禁止限时消息。 - 群组成员可以不可逆地删除已发送的消息。(24小时) - 群组成员可以私信。 + 此群中禁止成员之间私信。 + 限时消息被禁止。 + 成员可以不可逆地删除已发送的消息。(24小时) + 成员可以发送私信。 限时消息 在最近的应用程序中隐藏应用程序屏幕。 离开 @@ -439,13 +434,10 @@ 实时消息! 链接预览图片 无效的二维码 - 以后可以通过设置进行更改。 - 它可能在以下情况发生: -\n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。 -\n2. 消息解密失败,因为您或您的联系人使用了旧的数据库备份。 -\n3.连接被破坏。 - 离开群组? - 通过您的群组链接邀请 + 它如何影响电量 + 它可能在以下情况发生: \n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。 \n2. 消息解密失败,因为你或你的联系人使用了旧的数据库备份。 \n3.连接被破坏。 + 离开群? + 通过你的群链接邀请 本地名称 无效的消息格式 无效数据 @@ -454,9 +446,9 @@ 无效的连接链接 斜体 已邀请 - 邀请加入群组 + 邀请加入群 加入 - 加入群组? + 加入群? 邀请成员 已离开 浅色 @@ -467,25 +459,24 @@ 无效聊天 无效的服务器地址! 邀请成员 - 离开群组 + 离开群 仅本地配置文件数据 即时通知 即时通知! - 使用您的凭据登录 + 使用你的凭据登录 大文件! 链接无效! 已离开 - 此群组中禁止不可逆消息移除。 + 不可逆消息删除被禁止。 不可逆消息移除 实时消息 消息正文 等待确认中…… 即时 - 只有您的联系人才可以发送限时消息。 + 只有你的联系人才可以发送限时消息。 显示联系人和消息 只显示联系人 - 为保护您的信息,请打开 SimpleX 锁定。 -\n在启用此功能之前,系统将提示您完成身份验证。 + 为保护你的信息,请打开 SimpleX 锁定。 \n在启用此功能之前,系统将提示你完成身份验证。 聊天 分享文件…… 分享媒体…… @@ -493,16 +484,16 @@ 设置联系人姓名…… 已收到回复…… 已受到确认…… - 双层端到端加密 发送的消息。]]> + 仅客户端设备存储用户个人资料、联系人、群和消息。 视频通话(非端到端加密) 定期 私密通知 应用程序运行时 无端到端加密 显示 - 您只能在一台设备上使用最新版本的聊天数据库,否则您可能会停止接收来自某些联系人的消息。 + 你只能在一台设备上使用最新版本的聊天数据库,否则你可能会停止接收来自某些联系人的消息。 新密码…… - 该角色将更改为“%s”。群组中每个人都会收到通知。 + 该角色将更改为 %s。群中每个人都会收到通知。 SimpleX 锁定 定期通知 定期启动 @@ -512,8 +503,8 @@ 启动中…… 禁止发送限时消息。 - 只有您可以发送限时消息。 - 只有您的联系人能不可逆地删除消息(您可以将它们标记为删除)。(24小时) + 只有你可以发送限时消息。 + 只有你的联系人能不可逆地删除消息(你可以将它们标记为删除)。(24小时) 禁止发送限时消息。 通知只会在应用程序停止之前发送! @@ -525,18 +516,18 @@ 通知 正在接收消息…… 要接收通知,请输入数据库密码 - SimpleX 后台服务 ——它每天使用百分之几的电池。]]> - 您的设置 - 允许 SimpleX 在后台运行。 否则,通知将被禁用。]]> + SimpleX 在后台运行而不是使用推送通知。]]> + 你的设置 + 允许它 来立即接收通知。]]> 通知预览 需要密码 定期通知被禁用! 在应用程序打开时运行 显示预览 - 该应用程序会定期获取新消息——它每天会消耗百分之几的电量。该应用程序不使用推送通知——您设备中的数据不会发送到服务器。 - 您的联系人可以允许完全删除消息。 + 该应用程序会定期获取新消息——它每天会消耗百分之几的电量。该应用程序不使用推送通知——你设备中的数据不会发送到服务器。 + 你的联系人可以允许完全删除消息。 已发送的消息将在设定的时间后被删除。 - 您的聊天数据库 + 你的聊天数据库 密码错误! 保存 打开 @@ -545,50 +536,48 @@ 打开聊天 更改数据库密码的尝试未完成。 移除 - + 保存 - + 移除 - 您必须在每次应用程序启动时输入密码——它不存储在设备上。 + 你必须在每次应用程序启动时输入密码——它不存储在设备上。 设置密码来导出 请输入正确的当前密码。 更新数据库密码 - 您的聊天数据库未加密——设置密码来保护它。 - 请安全地保存密码,如果您丢失了密码,您将无法访问聊天。 - 请安全地保存密码,如果您丢失了密码,您将无法更改它。 + 你的聊天数据库未加密——设置密码来保护它。 + 请安全地保存密码,如果你丢失了密码,你将无法访问聊天。 + 请安全地保存密码,如果你丢失了密码,你将无法更改它。 数据库密码错误 保存 更新 打开 SimpleX Chat 来接听电话 视频通话 - %1$s 想通过以下方式与您联系 + %1$s 想通过以下账户与你连接 拒接来电 点对点 错误:%s - 扫描视频通话中的二维码,或者您的联系人可以分享邀请链接。]]> - 您的通话 + 扫描视频通话中的二维码,或者你的联系人可以分享邀请链接。]]> + 你的通话 通过中继 未接来电 拒接来电 语音消息 语音消息 SimpleX Chat 通话 - 在视频通话中出示二维码,或分享链接。]]> - 您的聊天资料 + 在视频通话中出示二维码,或分享链接。]]> + 你的聊天资料 未接来电 待定来电 - 除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 -\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。 - 您已经连接到 %1$s。 - 您的聊天资料将被发送 -\n给您的联系人 + 除非你的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 \n如果要连接,请让你的联系人创建另一个连接链接,并检查你的网络连接是否稳定。 + 你已经连接到 %1$s。 + 你的聊天资料将被发送 \n给你的联系人 资料和服务器连接 更新网络设置? - 只有您可以不可逆地删除消息(您的联系人可以将它们标记为删除)。(24小时) + 只有你可以不可逆地删除消息(你的联系人可以将它们标记为删除)。(24小时) 重新启动应用程序以创建新的聊天资料。 服务器需要授权才能创建队列,检查密码 测试在步骤 %s 失败。 - 您已经有一个显示名相同的聊天资料。请选择另一个名字。 + 你已经有一个显示名相同的聊天资料。请选择另一个名字。 已发送 静音 资料图片 @@ -596,33 +585,32 @@ 设置 未知错误 未知数据库错误:%s - 已更新的群组资料 + 已更新的群资料 已删除 %1$s - 您删除了 %1$s - 您的个人资料将发送给您收到此链接的联系人。 + 你删除了 %1$s + 你的个人资料将发送给你收到此链接的联系人。 正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。 - 您已连接到用于接收该联系人消息的服务器。 - 您分享了一次性链接 - 很可能此联系人已经删除了与您的联系。 + 你已连接到用于接收该联系人消息的服务器。 + 你分享了一次性链接 + 很可能此联系人已经删除了与你的联系。 资料图片占位符 - 您当前聊天资料的新连接服务器 - 您当前的资料 - 您的资料存储在您的设备上并且仅与您的联系人共享。SimpleX 服务器无法看见您的资料。 - 您的资料、联系人和发送的消息存储在您的设备上。 - 该资料仅与您的联系人共享。 + 你当前聊天资料的新连接服务器 + 你当前的资料 + 你的资料存储在你的设备上并且仅与你的联系人共享。SimpleX 服务器无法看见你的资料。 + 你的资料、联系人和发送的消息存储在你的设备上。 + 该资料仅与你的联系人共享。 开启 - 此操作无法撤消——您的个人资料、联系人、消息和文件将不可逆地丢失。 - 此设置适用于您当前聊天资料中的消息 + 此操作无法撤消——你的个人资料、联系人、消息和文件将不可逆地丢失。 + 此设置适用于你当前聊天资料中的消息 恢复数据库错误 恢复 - 您发送了群组邀请 - 您拒绝了群组邀请 - 您当前的聊天数据库将被删除并替换为导入的数据库。 -\n此操作无法撤消——您的个人资料、联系人、消息和文件将不可逆地丢失。 + 你发送了群邀请 + 你拒绝了群邀请 + 你当前的聊天数据库将被删除并替换为导入的数据库。 \n此操作无法撤消——你的个人资料、联系人、消息和文件将不可逆地丢失。 已邀请 %1$s - 保存群组资料 + 保存群资料 服务器地址中的证书指纹可能不正确 - 请使用 %1$s 检查您的网络连接,然后重试。 + 请使用 %1$s 检查你的网络连接,然后重试。 多个聊天资料 数据库不能正常工作。点击了解更多 消息传递错误 @@ -632,30 +620,29 @@ %d 秒 SimpleX 是如何工作的 确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。 - 如果SimpleX没有用户标识符,它是怎样传递信息的?]]> 确保 SMP 服务器地址格式正确、每行分开且不重复。 Markdown 帮助 标记为已验证 建立私密连接 以 %s 身份加入 - 如果您收到 SimpleX Chat 邀请链接,您可以在浏览器中打开它: + 如果你收到 SimpleX Chat 邀请链接,你可以在浏览器中打开它: 标记为已读 标记为未读 在消息中使用 Markdown 文件:%s - 成员将被移出群组——此操作无法撤消! + 成员将被移出群——此操作无法撤消! 消息草稿 k 标记为已删除 %d 星期 - 您将停止接收来自该群组的消息。聊天记录将被保留。 + 你将停止接收来自该群的消息。聊天记录将被保留。 成员 成员 %d 星期 %d 分钟 - %d 月 + %d 个月 网络和服务器 - 网络设置 + 高级设置 已被管理员移除 Onion 主机将在可用时使用。 将不会使用 Onion 主机。 @@ -665,9 +652,7 @@ 未选择联系人 一次性邀请链接 关闭 - 连接需要 Onion 主机。 -\n请注意:如果没有 .onion 地址,您将无法连接到服务器。 - Onion 主机将在可用时使用。 + 连接需要 Onion 主机。 \n请注意:如果没有 .onion 地址,你将无法连接到服务器。 从不 已提供 %s 已提供 %s:%2s @@ -675,8 +660,8 @@ 一次性邀请链接 好的 没有细节 - (仅由群组成员存储) - 只有您可以发送语音消息。 + (仅由群成员存储) + 只有你可以发送语音消息。 消息将被删除——此操作无法撤消! 一次只能发送10张图片 更多 @@ -686,8 +671,6 @@ 没有联系人可添加 网络状态 关闭 - 将不会使用 Onion 主机。 - 连接需要 Onion 主机。 没有收到或发送的文件 发送人已取消文件传输。 分享 @@ -701,9 +684,8 @@ 必须 保存并通知联系人 保存并通知联系人 - 在我们的 GitHub 仓库中阅读更多内容。 拒绝 - 为了保护隐私,而不是所有其他平台使用的用户 ID,SimpleX 具有消息队列的标识符,每个联系人都是分开的。 + 为了保护隐私,SimpleX 对你的每一个联系人使用不同的 ID。 TCP 连接超时 收到,禁止 设定1天 @@ -711,40 +693,38 @@ 在浏览器中打开链接可能会降低连接的隐私和安全性。SimpleX 上不受信任的链接将显示为红色。 恢复数据库备份后请输入之前的密码。 此操作无法撤消。 请更新应用程序并联系开发者。 - 开源协议和代码——任何人都可以运行服务器。 + 任何人都可以托管服务器。 粘贴 PING 次数 禁止发送语音消息。 PING 间隔 - 请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。 + 请检查你使用的链接是否正确,或者让你的联系人给你发送另一个链接。 协议超时 拒绝 回复 重置为默认 运行聊天程序 - 保存存档 扫码 - 从您联系人的应用程序中扫描安全码。 + 从你联系人的应用程序中扫描安全码。 安全码 秘密 安全评估 SimpleX 消息 %s 未验证 感谢用户——通过 Weblate 做出贡献! - 第一个没有任何用户标识符的平台——专为隐私保护设计。 + 没有用户标识符。 完全去中心化 - 仅对成员可见。 图像无法解码。 请尝试不同的图像或联系开发者。 主题 此操作无法撤消——所有接收和发送的文件和媒体都将被删除。 低分辨率图片将保留。 - 角色将更改为“%s”。 该成员将收到新的邀请。 + 角色将更改为%s。 该成员将收到新的邀请。 此操作无法撤消——早于所选的发送和接收的消息将被删除。 这可能需要几分钟时间。 此二维码不是链接! 接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。 此链接不是有效的连接链接! 开始新的聊天 - 要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。 + 要与你的联系人验证端到端加密,请比较(或扫描)你设备上的代码。 取消静音 - 更新 .onion 主机设置? 更新传输隔离模式? (从剪贴板扫描或粘贴) 保护队列 @@ -755,31 +735,31 @@ 太多图片! 待办的 更改接收地址? - 请让您的联系人启用发送语音消息。 + 请让你的联系人启用发送语音消息。 录制语音消息 发消息 重置 发送 - 发送实时消息——它会在您键入时为收件人更新 + 发送实时消息——它会在你键入时为收件人更新 开始新聊天 - (与您的联系人分享) + (与你的联系人分享) 通过链接连接 设置联系人姓名 - 您接受的连接将被取消! - 您与之共享此链接的联系人将无法连接! + 你接受的连接将被取消! + 你与之共享此链接的联系人将无法连接! 显示二维码 发送问题和想法 - 保护您的隐私和安全的消息传递和应用程序平台。 + 保护你的隐私和安全的消息传递和应用程序平台。 删去 - 人们只能通过您共享的链接与您建立联系。 + 你决定谁可以连接。 下一代私密通讯软件 - 粘贴您收到的链接 + 粘贴你收到的链接 已跳过消息 支持 SIMPLEX CHAT 发送链接预览 SOCKS 代理 停止聊天程序? - 停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,您将无法收发消息。 + 停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,你将无法收发消息。 恢复数据库备份 恢复数据库备份? 删除成员 @@ -804,7 +784,7 @@ 测试服务器 SimpleX 联系地址 SimpleX 一次性邀请 - SimpleX 群组链接 + SimpleX 群链接 SimpleX 链接 发送人可能已删除连接请求。 预设服务器 @@ -815,13 +795,12 @@ 跳过邀请成员 转变 禁止发送语音消息。 - 只有您的联系人可以发送语音消息。 + 只有你的联系人可以发送语音消息。 禁止向成员发送私信。 保护应用程序屏幕 主题 - 停止聊天以启用数据库操作。 %s 秒 - 该群组已不存在。 + 该群已不存在。 点击加入 停止 重新启动应用程序以使用导入的聊天数据库。 @@ -829,9 +808,7 @@ 已删除 角色 - 恢复 重置颜色 - 保存颜色 减少电池使用量 为了保护时区,图像/语音文件使用 UTC。 使用聊天 @@ -840,110 +817,107 @@ 停止聊天程序 权限被拒绝! 点击按钮 - 感谢您安装 SimpleX Chat! + 感谢你安装 SimpleX Chat! 相机 预设服务器地址 扫描服务器二维码 服务器测试失败! 一些服务器未通过测试: 在 GitHub 上加星 - 保存并通知群组成员 + 保存并通知群成员 扬声器关闭 扬声器开启 - 已将您移除 + 已将你移除 更新设置会将客户端重新连接到所有服务器。 系统 - 对方会在您键入时看到更新。 + 对方会在你键入时看到更新。 查看安全码 语音消息 (%1$s) 等待图像中 欢迎! 欢迎 %1$s! - 当您的联系人设备在线时,您将可以连接,请稍等或稍后查看! + 当你的联系人设备在线时,你将可以连接,请稍等或稍后查看! 评价此应用程序 使用 SOCKS 代理? %d 个文件,总大小为 %s - 您已加入此群组 - 您被邀请加入群组 + 你已加入此群 + 你被邀请加入群 默认(%s) 此聊天中禁止语音消息。 - 语音信息在该群组中被禁用。 + 语音信息被禁止。 验证安全码 使用 SimpleX Chat 服务器。 通过 %1$s - 邀请至群组 %1$s + 邀请至群 %1$s SimpleX 地址 SimpleX 团队 - %1$s 成员 + %1$s 名成员 - 接收消息,您的联系人 - 您用来向他们发送消息的服务器。]]> - 您将在组主设备上线时连接到该群组,请稍等或稍后再检查! - 当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。 - 创建于 %1$s - 连接到 SimpleX Chat 开发者提出任何问题并接收更新 。]]> - 您已接受连接 - 您的 SMP 服务器 - %1$d 已跳过消息 + 你将在组主设备上线时连接到该群,请稍等或稍后再检查! + 当你启动应用或在应用程序驻留后台超过30 秒后,你将需要进行身份验证。 + 连接到 SimpleX Chat 开发者提出任何问题并接收更新 。]]> + 你已接受连接 + 你的 SMP 服务器 + %1$d 条已跳过消息 %ds 更新内容 - 您被邀请加入群组 - 您没有聊天记录 + 你被邀请加入群 + 你没有聊天记录 等待图像中 语音消息 语音消息禁止发送! - 您需要允许您的联系人发送语音消息才能发送它们。 + 你需要允许你的联系人发送语音消息才能发送它们。 扫描二维码 - 您邀请了您的联系人 - 想要与您连接! - 您的联系人需要在线才能完成连接。 -\n您可以取消此连接并删除联系人(稍后尝试使用新链接)。 + 你邀请了一名联系人 + 想要与你连接! + 你的联系人需要在线才能完成连接。 \n你可以取消此连接并删除联系人(稍后尝试使用新链接)。 SimpleX 标志 - 您的 SimpleX 地址 + 你的 SimpleX 地址 为终端安装 SimpleX Chat 使用 SimpleX Chat 服务器? - 我们不会在服务器上存储您的任何联系人或消息(一旦发送)。 + 我们不会在服务器上存储你的任何联系人或消息(一旦发送)。 WebRTC ICE 服务器 - 中继服务器保护您的 IP 地址,但它可以观察通话的持续时间。 - 中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。 - 您的 ICE 服务器 + 中继服务器保护你的 IP 地址,但它可以观察通话的持续时间。 + 中继服务器仅在必要时使用。其他人可能会观察到你的IP地址。 + 你的 ICE 服务器 视频关闭 - 您可以通过应用设置/数据库或重启应用开始聊天。 - 您将 %s 的角色更改为 %s - 您将自己的角色更改为 %s - 您已更改地址 - 您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。 + 你可以通过应用设置/数据库或重启应用开始聊天。 + 你将 %s 的角色更改为 %s + 你将自己的角色更改为 %s + 你已更改地址 + 你可以共享链接或二维码——任何人都可以加入该群。如果你稍后将其删除,你不会失去该组的成员。 间接(%1$s) - 在移动应用程序中打开按钮。]]> + 在移动应用程序中打开按钮。]]> SimpleX 你将连接到所有群成员。 - 通过群组链接 + 通过群链接 通过一次性链接 通过联系地址链接 通过浏览器 - 您的服务器 + 你的服务器 当可用时 使用 .onion 主机 - 您的 ICE 服务器 + 你的 ICE 服务器 simplexmq: v%s (%2s) - 您的聊天由您掌控! - 您可以使用 markdown 来编排消息格式: + 你的聊天由你掌控! + 你可以使用 markdown 来编排消息格式: %dh %d 天 %dw - 您被邀请加入群组。 加入以与群组成员联系。 - 你加入了这个群组。连接到邀请组成员。 - 您更改了 %s 的地址 - 您已离开 - %d 已选择联系人 - 您允许 + 你被邀请加入群。 加入以与群成员联系。 + 你加入了这个群。连接到邀请组成员。 + 你更改了 %s 的地址 + 你已离开 + 已选择 %d 名联系人 + 你允许 带有可选的欢迎消息。 %dm %dmth 等待文件中 - 您的联系人发送的文件大于当前支持的最大大小 (%1$s). - 当您的连接请求被接受后,您将可以连接,请稍等或稍后检查! + 你的联系人发送的文件大于当前支持的最大大小 (%1$s). + 当你的连接请求被接受后,你将可以连接,请稍等或稍后检查! 使用服务器 - 您的服务器地址 + 你的服务器地址 视频开启 最多 40 秒,立即收到。 验证连接安全 @@ -952,12 +926,12 @@ 将为所有成员删除该消息。 该消息将对所有成员标记为已被管理员移除。 删除成员消息? - 观察者 - 您是观察者 - 更新群组链接错误 - 您无法发送消息! + 观察员 + 你是观察者 + 更新群链接错误 + 你无法发送消息! 初始角色 - 请联系群组管理员。 + 请联系群管理员。 系统 用于显示的密码 保存个人资料密码 @@ -974,19 +948,19 @@ 更多改进即将推出! 现在管理员可以: \n- 删除成员的消息。 -\n- 禁用成员(“观察员”角色) - 使用密码保护您的聊天资料! +\n- 禁用成员(观察员角色) + 使用密码保护你的聊天资料! 确认密码 更新用户隐私错误 保存用户密码错误 在搜索中输入密码 - 群组欢迎消息 - 群组管理员移除 + 群欢迎消息 + 群管理员移除 隐藏的个人资料密码 隐藏的聊天资料 隐藏个人资料 保存服务器? - 要显示您的隐藏的个人资料,请在您的聊天个人资料页面的搜索字段中输入完整密码。 + 要显示你的隐藏的个人资料,请在你的聊天个人资料页面的搜索字段中输入完整密码。 保存欢迎信息? 点击以激活个人资料。 取消隐藏 @@ -995,8 +969,8 @@ 感谢用户——通过 Weblate 做出贡献! 解除静音 欢迎消息 - 当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。 - 您可以隐藏或静音用户配置文件——长按以显示菜单。 + 当静音配置文件处于活动状态时,你仍会收到来自静音配置文件的电话和通知。 + 你可以隐藏或静音用户配置文件——长按以显示菜单。 欢迎消息 确认数据库升级 实验性 @@ -1007,13 +981,13 @@ 数据库版本比应用程序更新,但无法降级迁移:%s 降级并打开聊天 隐藏: - 文件将在您的联系人完成上传后收到。 + 文件将在你的联系人完成上传后收到。 数据库版本不兼容 迁移:%s - 图片将在您的联系人完成上传后收到。 + 图片将在你的联系人完成上传后收到。 显示开发者选项 升级并打开聊天 - 警告:您可能会丢失部分数据! + 警告:你可能会丢失部分数据! 迁移确认无效 显示: 删除个人资料 @@ -1025,15 +999,15 @@ 过多视频! 视频 等待视频中 - 视频将在您的联系人在线时收到,请稍等或稍后查看! + 视频将在你的联系人在线时收到,请稍等或稍后查看! 等待视频中 视频已发送 要求接收视频 - 视频将在您的联系人完成上传后收到。 + 视频将在你的联系人完成上传后收到。 服务器需要授权来上传,检查密码 上传文件 XFTP 服务器 - 您的 XFTP 服务器 + 你的 XFTP 服务器 Use .onion hosts 设置为否。]]> 使用 SOCKS 代理 端口 @@ -1056,7 +1030,7 @@ 没有应用程序密码 密码输入 请牢记或妥善保管——丢失的密码将无法恢复! - 您可以通过设置开启 SimpleX 锁定。 + 你可以通过设置开启 SimpleX 锁定。 身份验证 身份验证失败 更改密码 @@ -1074,18 +1048,18 @@ 密码已设置! 系统 未启用 SimpleX 锁定! - 您的身份无法验证,请再试一次。 + 你的身份无法验证,请再试一次。 身份验证已取消 当前密码 立即 错误消息散列 错误消息 ID - %1$d 消息解密失败。 - %1$d 已跳过消息。 - 当您或您的连接使用旧数据库备份时,可能会发生这种情况。 + %1$d 条消息解密失败。 + 跳过了 %1$d 条消息。 + 当你或你的连接使用旧数据库备份时,可能会发生这种情况。 解密错误 请向开发者报告。 - 上一条消息的散列不同。 + 上条消息的哈希值不同。 下一条消息的 ID 不正确(小于或等于上一条)。 \n它可能是由于某些错误或连接被破坏才发生。 停止文件 @@ -1094,10 +1068,10 @@ 停止接收文件? 即将停止接收文件。 停止 - 撤销文件 - 撤销文件? + 吊销文件 + 吊销文件? 文件将从服务器中删除。 - 撤销 + 吊销 音频/视频通话 " \n在 v5.1 版本中可用" @@ -1108,30 +1082,30 @@ 最大 1gb 的视频和文件 快速且无需等待发件人在线! 禁止音频/视频通话。 - 您和您的联系人都可以拨打电话。 - 只有您可以拨打电话。 - 只有您的联系人可以拨打电话。 - 允许您的联系人与您进行语音通话。 - 仅当您的联系人允许时才允许呼叫。 + 你和你的联系人都可以进行呼叫。 + 只有你可以进行呼叫。 + 只有你的联系人可以进行呼叫。 + 允许联系人呼叫你。 + 允许通话,前提是你的联系人允许它们。 禁止音频/视频通话。 1分钟 一次性链接 - 您和您的联系人都可以添加消息回应。 + 你和你的联系人都可以添加消息回应。 允许消息回应。 - 只有您的联系人允许时才允许消息回应。 + 允许消息回应,前提是你的联系人允许它们。 应用程序密码被替换为自毁密码。 更改自毁模式 关于 SimpleX 地址 继续 - 您的所有联系人将保持连接。个人资料更新将发送给您的联系人。 + 你的所有联系人将保持连接。个人资料更新将发送给你的联系人。 自动接受 额外的次要 背景 5分钟 30秒 地址 - 将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。 - 允许您的联系人添加消息回应。 + 将地址添加到你的个人资料,以便你的联系人可以与其他人共享。个人资料更新将发送给你的联系人。 + 允许你的联系人添加消息回应。 额外的强调色 已删除所有应用程序数据。 已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。 @@ -1147,7 +1121,7 @@ 消失于 设置地址错误 自定义主题 - 创建一个地址,让人们与您联系。 + 创建一个地址,让人们与你联系。 创建 SimpleX 地址 输入欢迎消息……(可选) 不创建地址 @@ -1165,9 +1139,9 @@ 已删除于:%s 消失于:%s 禁止消息回应。 - 只有您可以添加消息回应。 - 群组成员可以添加信息回应。 - 该群组禁用了消息回应。 + 只有你可以添加消息回应。 + 成员可以添加信息回应。 + 消息回应被禁止。 自毁密码已更改! 自毁密码已启用! 设置密码 @@ -1175,7 +1149,7 @@ 已发信息 历史记录 发送 - 如果您在打开应用时输入该密码,所有应用程序数据将被不可逆地删除! + 如果你在打开应用时输入该密码,所有应用程序数据将被不可逆地删除! 新的显示名: 已被管理员移除于 已发送于 @@ -1185,18 +1159,18 @@ %s (当前) 发送于 %s 收到的信息 - 只有您的联系人可以添加消息回应。 + 只有你的联系人可以添加消息回应。 打开数据库中…… - 打开聊天资料 - 您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。 - 您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 - 如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。 + 更改聊天资料 + 你的联系人可以扫描二维码或使用应用程序中的链接来建立连接。 + 你可以将你的地址作为链接或二维码共享——任何人都可以连接到你。 + 如果你不能亲自见面,可以在视频通话中展示二维码,或分享链接。 了解更多 - 当人们请求连接时,您可以接受或拒绝它。 - 如果您以后删除您的地址,您不会丢失您的联系人。 + 当人们请求连接时,你可以接受或拒绝它。 + 如果你以后删除你的地址,你不会丢失你的联系人。 用户指南中阅读更多。]]> - 主题颜色 - 与您的联系人保持连接。 + 界面颜色 + 与你的联系人保持连接。 与联系人分享 邀请朋友 保存自动接受设置 @@ -1205,9 +1179,9 @@ 你好! \n用 SimpleX Chat 与我联系:%s 让我们一起在 SimpleX Chat 里聊天 - 您可以以后创建它 + 你可以以后创建它 分享地址 - 您可以与您的联系人分享该地址,让他们与 %s 联系。 + 你可以与你的联系人分享该地址,让他们与 %s 联系。 预览 导入主题 SimpleX @@ -1232,8 +1206,8 @@ 已发信息 消息回应 该聊天禁用了消息回应。 - 如果您在打开应用程序时输入自毁密码: - 个人资料更新将被发送给您的联系人。 + 如果你在打开应用程序时输入自毁密码: + 个人资料更新将被发送给你的联系人。 记录更新于 禁止消息回应。 已收到于 @@ -1245,10 +1219,10 @@ - 语音消息最长5分钟。 \n- 自定义限时消息。 \n- 编辑消息历史。 - 导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。 + 导入过程中发生了一些非致命错误: 应用程序 重启 - 通知将停止工作直到您重启应用程序 + 通知将停止工作直到你重启应用程序 关闭? 关闭 中止地址更改错误 @@ -1258,9 +1232,9 @@ 中止地址更改 允许发送文件和媒体。 文件和媒体 - 只有组主可以启用文件和媒体。 - 此群组中禁止文件和媒体。 - 群组成员可以发送文件和媒体。 + 只有群主可以启用文件和媒体。 + 文件和媒体被禁止。 + 成员可以发送文件和媒体。 禁止发送文件和媒体。 禁止文件和媒体! 无过滤聊天 @@ -1277,9 +1251,7 @@ 选择一个文件 联系人 协调加密中… - - 更稳定的消息送达. -\n- 更好的群组. -\n- 还有更多! + - 更稳定的消息传送. \n- 更好的群. \n- 还有更多! 一个新的随机个人档案将被分享。 与 %s 协调加密中… 该功能还没支持。请尝试下一个版本。 @@ -1290,10 +1262,10 @@ %s: %s 敬请期待! 数据库将被加密,密码将存储在设置中。 - 您可以稍后在“设置”中启用它 - 对所有群组关闭 + 你可以稍后在“设置”中启用它 + 对所有群关闭 无送货信息 - 您的个人资料 %1$s 将被共享。 + 你的个人资料 %1$s 将被共享。 将为所有联系人启用送达回执功能。 打开应用程序设置 为所有组启用 @@ -1310,13 +1282,12 @@ %s 在 %s 禁用回执? 重新协商加密? - 可以在联系人和群组设置中覆盖它们。 + 可以在联系人和群设置中覆盖它们。 对所有联系人关闭 - 随机密码以明文形式存储在设置中。 -\n您可以稍后更改。 + 随机密码以明文形式存储在设置中。 \n你可以稍后更改。 已禁用 %d 组的送达回执功能 需要为 %s 重新协商加密 - SimpleX 无法在后台运行。只有在应用程序运行时,您才会收到通知。 + SimpleX 无法在后台运行。只有在应用程序运行时,你才会收到通知。 启用(保留覆盖) 即将更新数据库加密密码并将其存储在设置中。 使用当前配置文件 @@ -1328,16 +1299,16 @@ 即使在对话中禁用。 使用随机密码 无后台通话 - 您可以稍后通过应用程序隐私和安全设置启用它们。 + 你可以稍后通过应用程序隐私和安全设置启用它们。 在设置中保存密码 启用 - 该群组成员超过 %1$d ,未发送送达回执。 + 该群成员超过 %1$d ,未发送送达回执。 修复连接? - 我们错过的第二个\"√\"!✅ + 我们错过的第二个"√"!✅ 设定数据库密码 - 为群组禁用回执吗? + 为群禁用回执吗? %s、%s 和 %s 已连接 - 修复群组成员不支持的问题 + 修复群成员不支持的问题 已为 %d 组启用送达回执功能 重新协商 禁用(保留覆盖) @@ -1352,18 +1323,18 @@ 修复连接 %s 和 %s 已连接 关闭 - 小群组(最多 20 人) + 小群(最多 20 人) 显示最近的消息 将送达回执发送给 启用已读回执时出错! 更改密码或重启应用后,密码将以明文形式保存在设置中。 - 粘贴您收到的链接以与您的联系人联系… + 粘贴你收到的链接以与你的联系人联系… 送达回执 - 没有选择聊天 + 没有选中的聊天 可以加密 重新协商加密 禁用(保留组覆盖) - 为群组启用回执吗? + 为群启用回执吗? 修复联系人不支持的问题 对 %s 加密正常 修复还原备份后的加密问题。 @@ -1374,7 +1345,7 @@ 连接请求将发送给该组成员。 密码以明文形式存储在设置中。 同步连接时出错 - 这些设置适用于您当前的配置文件 + 这些设置适用于你当前的配置文件 允许为 %s 重新协商加密 为所有人启用 需要重新协商加密 @@ -1385,14 +1356,12 @@ 全新桌面应用! 6种全新的界面语言 应用程序为新的本地文件(视频除外)加密。 - 发现和加入群组 + 发现和加入群 简化的隐身模式 阿拉伯语、保加利亚语、芬兰语、希伯莱语、泰国语和乌克兰语——得益于用户和Weblate。 在桌面应用里创建新的账号。💻 在连接时切换隐身模式。 - - 连接到目录服务(BETA)! -\n- 发送回执(至多20名成员)。 -\n- 更快,更稳定。 + - 连接到目录服务(BETA)! \n- 发送回执(至多20名成员)。 \n- 更快、更稳定。 打开 创建成员联系人时出错 发送私信来连接 @@ -1410,14 +1379,14 @@ 加入你的群吗? %1$s 群。]]> 这是你自己的一次性链接! - %d 条消息被标记为删除 + %d 条消息被标记为已删除 群已存在! 已经在连接了! 无法解码该视频。请尝试不同视频或联络开发者。 %s 已连接 及其他 %d 个事件 通过链接进行连接吗? - 已经加入了该群组! + 已经加入了该群! %s、 %s 和 %d 名成员 解封成员 连接到你自己? @@ -1496,13 +1465,13 @@ 从已链接移动设备加载文件时请稍候片刻 桌面应用版本 %s 不兼容此应用。 验证连接 - 屏蔽群组成员 - 使用随机身份创建群组 + 屏蔽群成员 + 使用随机身份创建群 连接移动端和桌面端应用程序!🔗 通过安全的、抗量子计算机破解的协议。 隐藏不需要的信息。 - 更佳的群组 - 匿名群组 + 更佳的群 + 匿名群 %s 连接断开]]> 加入速度更快、信息更可靠。 - 可选择通知已删除的联系人。 @@ -1535,10 +1504,10 @@ 不给新成员发送历史消息。 或者显示此码 给新成员发送了最多 100 条历史消息。 - 您扫描的码不是 SimpleX 链接的二维码。 - 您粘贴的文本不是 SimpleX 链接。 + 你扫描的码不是 SimpleX 链接的二维码。 + 你粘贴的文本不是 SimpleX 链接。 启用相机访问 - 您可以在连接详情中再次查看邀请链接。 + 你可以在连接详情中再次查看邀请链接。 保留未使用的邀请吗? 分享此一次性邀请链接 建群: 来建立新群。]]> @@ -1651,7 +1620,7 @@ 应用数据迁移 通过二维码迁移到另一部设备。 画中画通话 - 更安全的群组 + 更安全的群 通话时使用本应用 迁移到此处 或粘贴存档链接 @@ -1736,9 +1705,9 @@ 不允许语音消息 SimpleX 链接 允许发送 SimpleX 链接。 - 群成员可发送 SimpleX 链接。 + 成员可发送 SimpleX 链接。 禁止发送 SimpleX 链接 - 此群禁止 SimpleX 链接。 + SimpleX 链接被禁止。 所有者 启用对象 管理员 @@ -1769,4 +1738,641 @@ 个人资料图 改变个人资料图形状 方形、圆形、或两者之间的任意形状 - \ No newline at end of file + 超出了额度 — 收信人没收到之前发送的消息。 + 目标服务器错误:%1$s + 错误:%1$s + 转发服务器:%1$s +\n错误:%2$s + 消息传输警告 + 网络问题 — 许多发送消息的尝试后,消息过期了。 + 密钥错误或连接未知 — 连接被删除的可能性最大。 + 始终 + 从不 + 未知服务器 + 始终使用私密路由。 + 不使用私密路由。 + 在未知服务器上使用私密路由。 + 当 IP 地址不受保护时,在未知服务器上使用私密路由。 + 当 IP 隐藏时 + + + 当你的服务器或目标服务器不支持私密路由时直接发送消息。 + 备用消息路由 + 显示消息状态 + 为了保护你的 IP 地址,私密路由使用你的 SMP 服务器来传送消息。 + 私密消息路由 + 私密路由 + 当 IP 地址受保护且你的服务器或目标服务器不支持私密路由时,直接发送消息。 + 服务器地址和网络设置不兼容。 + 允许降级 + 服务器版本和网络设置不兼容。 + 未受保护 + 不直接发送消息,即便你的服务器或目标服务器不支持私密路由。 + 转发服务器:%1$s +\n目标服务器错误:%2$s + 消息路由模式 + 未知服务器! + 没有 Tor 或 VPN,这些 XFTP 中继可以看到你的 IP 地址: +\n%1$s. + 没有 Tor 或 VPN,文件服务器可以看到你的 IP 地址。 + 保护 IP 地址 + 文件 + 应用将请求确认来自未知服务器的下载(.onion 或启用 SOCKS 代理时除外)。 + 个人资料主题 + 在新窗口中显示聊天列表 + 所有颜色模式 + 应用到 + + 颜色模式 + 深色 + 深色模式 + 深色模式颜色 + 填充 + 适配 + 下午好! + 早上好! + 浅色 + 浅色模式 + 收到的回复 + 删除图片 + 已发送回复 + 设置默认主题 + 系统 + 壁纸强调色 + 壁纸背景色 + 额外的强调色2 + 高级设置 + 聊天颜色 + 重复 + 聊天主题 + 重置颜色 + 缩放 + Webview 初始化失败。更新你的系统到新版本。请联系开发者。 +\n错误:%s + 保护你的真实 IP 地址。不让你的联系人选择的消息中继看到它。 \n在*网络&服务器*设置中开启。 + 确认来自未知服务器的文件。 + 安全地接收文件 + 改进了消息传递 + 让你的聊天看上去不同! + 私密消息路由🚀 + 新的聊天主题 + 波斯语用户界面 + 降低电池用量 + 主题 + 重置为应用主题 + 重置为用户主题 + 发送调试 + 消息队列信息 + 消息队列信息:%1$s +\n +\n上一则收到的信息:%2$s + + 未找到文件 - 最有可能的情况是文件被删或被取消了 + 错误的密钥或未知的文件块地址 - 最可能的情况是文件被删了。 + 文件错误 + 文件服务器错误:%1$s + 文件状态 + 消息状态 + 临时性文件错误 + 文件状态:%s + 消息状态:%s + 请检查移动设备和桌面设备连接到的是同一个本地网络,且桌面防火墙允许连接。 +\n请和开发者分享任何其他问题。 + 此链接用于另一台移动设备,请在桌面上创建新的链接。 + 复制错误 + 无法发送消息 + 选择的聊天首选项禁止此条消息。 + 请稍后尝试。 + 私密路由出错 + 已转发的消息 + 尚无直接连接,消息由管理员转发。 + 其他 SMP 服务器 + 其他 XFTP 服务器 + 扫描/粘贴链接 + 显示百分比 + 不活跃 + 缩放 + 所有配置文件 + 文件 + 没有信息,试试重新加载 + 服务器信息 + 尝试 + 已连接 + 已连接的服务器 + 连接中 + 活跃连接 + 详细统计数据 + 详情 + 已下载 + 错误 + 重连服务器出错 + 重连服务器出错 + 重设统计数据出错 + 错误 + 收到的消息 + 消息接收 + 待连接 + 先前连接的服务器 + 已代理的服务器 + 接收到的消息 + 接收总计 + 接收错误 + 重连 + 重连服务器? + 重连服务器? + 重连服务器强制消息传输。这会使用额外流量。 + 重置所有统计数据 + 重置所有统计数据吗? + 直接发送 + 已发送消息 + 发送总计 + 通过代理发送 + 服务器统计数据将被重置。此操作无法撤销! + XFTP 服务器 + 认可出错 + 块已删除 + 块已下载 + 已完毕 + 连接数 + 已创建 + 解密出错 + 已删除 + 删除错误 + 已下载的文件 + 下载出错 + 重复 + 已过期 + 其他 + 其他错误 + 已代理 + 已受保护 + 发送错误 + 服务器地址 + 大小 + 已上传的文件 + 上传出错 + 重新连接所有已连接的服务器来强制消息传输。这会使用额外流量。 + 你没有连接到这些服务器。私密路由被用于向它们传输消息。 + 重连所有服务器 + 重置 + 服务器地址不兼容网络设置:%1$s。 + 起始自 %s。 + 自 %s 起 \n所有数据均私密地保存在你的设备上. + 已订阅 + 已认可 + 服务器版本不兼容你的应用:%1$s. + 信息主体 + SMP 服务器 + 统计数据 + 订阅错误 + 总计 + 块已上传 + 订阅被忽略 + 已配置的 SMP 服务器 + 已配置的 XFTP 服务器 + 当前配置文件 + 传输会话 + 已上传 + 已停用 + 字体大小 + 成员不活跃 + 如果成员变得活跃,可能会在之后传输消息。 + 发送的消息 + 打开服务器设置 + 检查更新 + 检查更新 + 停用 + 已停用 + 正在下载应用更新,不要关闭应用 + 下载 %s(%s) + 打开文件位置 + 请重启应用。 + 稍后提醒 + 跳过此版本 + 稳定版 + 有更新可用:%s + 取消了更新下载 + 要接收新版本通知,请打开“定期检查稳定或测试版本”。 + 应用更新已下载 + 测试版 + 安装成功 + 安装更新 + %1$s 的目的地服务器版本不兼容转发服务器 %2$s. + 转发服务器 %1$s 连接目的地服务器 %2$s 失败。请稍后尝试。 + 转发服务器地址不兼容网络设置:%1$s。 + 转发服务器版本不兼容网络设置:%1$s。 + %1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置。 + 连接转发服务器 %1$s 出错。请稍后尝试。 + 模糊媒体文件 + 中度 + 关闭 + 轻柔 + 强烈 + 消息 + 呼叫 + 联系人将被删除 - 无法撤销此操作! + 保留对话 + 只删除对话 + 删除了联系人! + 删除了对话! + 不通知删除 + 你仍可以在聊天列表中查看与 %1$s 的对话。 + 粘贴链接 + 联系人 + 单手应用工具栏 + 正在连接联系人,请等候或稍后检查! + 联系人被删除了。 + 要能够呼叫联系人,你需要先允许联系人进行呼叫。 + 请让你的联系人启用通话。 + 无法呼叫联系人 + 无法呼叫群成员 + 无法给群成员发消息 + 确认删除联系人? + 连接 + 邀请 + 没有过滤的联系人 + 打开 + 搜索 + 发送消息来开启通话。 + 你可以发消息给来自已存档联系人的 %1$s。 + 已存档的联系人 + 允许通话? + 通话被禁止! + 设置 + 视频 + 消息将被标记为删除。收信人将可以揭示这些消息。 + 删除成员的 %d 条消息吗? + 选择 + 写消息 + 什么也没选中 + 已选中 %d + 将对所有成员删除这些消息。 + 这些消息将对所有成员标记为受管制。 + 更快地连接到你的好友 + 连接和服务器状态。 + 最多同时删除 20 条消息 + 它保护你的 IP 地址和连接。 + 切换聊天列表: + 你可以在“外观”设置中更改它。 + 保存并重新连接 + TCP 连接 + 控制你的网络 + 单手聊天工具栏 + 存档之后要聊天的联系人。 + 用一只手使用本应用。 + 已导出聊天数据库 + 继续 + 媒体和文件服务器 + 消息服务器 + SOCKS 代理 + 某些文件未导出 + 你可以迁移导出的数据库。 + 你可以保存导出的存档。 + 重置所有提示 + 模糊以增强隐私。 + 新的媒体选项 + 从聊天列表播放。 + 自动升级应用 + 创建 + 从 GitHub 下载新版。 + 增大字体尺寸。 + 邀请 + 新的聊天体验 🎉 + 新消息 + 请检查 Simple X 链接是否正确。 + 无效链接 + 聊天数据库 + 系统模式 + 上传的数据库存档将永久性从服务器被删除。 + 确保代理配置正确 + 消息将被删除 - 此操作无法撤销! + 你的连接被移动到 %s,但在将你重定向到配置文件时发生了意料之外的错误。 + 代理不使用身份验证凭据 + 切换配置文件出错 + 代理身份验证 + 删除存档? + 选择聊天配置文件 + 保存代理出错 + 密码 + 每个连接使用不同的代理身份验证凭据。 + 每个配置文件使用不同的代理身份验证。 + 你的凭据可能以未经加密的方式被发送。 + 使用随机凭据 + 用户名 + 分享配置文件 + 转发消息出错 + 在你选中消息后这些消息已被删除。 + %1$d 个文件错误:\n%2$s + 其他 %1$d 个文件错误。 + %1$d 个文件未被下载。 + 转发 %1$s 条消息? + 仅转发消息不转发文件? + 没什么可转发的! + 仍有 %1$d 个文件在下载中。 + %1$d 个文件下载失败。 + 删除了 %1$d 个文件。 + 下载 + %1$s 条消息未被转发 + 转发消息… + 转发 %1$s 条消息 + 保存 %1$s 条消息 + 已静音 + 消息形状 + 拐角 + 尾部 + 初始化 WebView 出错。确保你安装了 WebView 且其支持的架构为 arm64。\n错误:%s + 应用会话 + 每次启动应用都会使用新的 SOCKS 凭据。 + 服务器 + 打开 Safari 设置/网站/麦克风,接着在 localhost 选择“允许”。 + 要进行通话,请允许使用设备麦克风。结束通话并尝试再次呼叫。 + 单击地址栏附近的“信息”按钮允许使用麦克风。 + 每个服务器都会使用新的 SOCKS 凭据。 + 更好的消息日期。 + 更佳的安全性✅ + 更佳的使用体验 + 可自定义消息形状。 + 一次转发最多20条消息。 + Trail of Bits 审核了 SimpleX 协议。 + 通话期间切换音频和视频。 + 对一次性邀请切换聊天配置文件。 + 更佳的通话 + 允许自行删除或管理员移除最多200条消息。 + 保存服务器出错 + 服务器配置有错误。 + 用于聊天资料 %s: + 无消息服务器 + 无私密消息路由服务器。 + 无消息接收服务器。 + 无文件发送服务器。 + 未送达的消息 + 无消息 + 连接安全性 + 和一位好友分享一次性链接 + 公开分享地址 + 在社媒上分享 SimpleX 地址。 + 你可以设置连接名称,用来记住和谁分享了这个链接。 + 可以通过任何消息应用安全分享 SimpleX 地址和一次性链接。 + 创建一次性链接 + 用于社交媒体 + 或者私下分享 + 服务器运营方 + 网络运营方 + 30 天后将接受已启用的运营方的条款。 + 继续 + 稍后审阅 + 选择要使用的网络运营方。 + 更新 + 你可以通过设置配置服务器。 + %s.]]> + 将于下列日期自动接受已启用的运营方的条款:%s。 + 预设服务器 + 你的服务器 + 接受条款的将来日期为:%s。 + 网络运营方 + 运营方 + %s 台服务器 + 网站 + 无法加载当前条款文本,你可以通过此链接审阅条款: + 使用 %s + 使用服务器 + %s.]]> + %s.]]> + 查看条款 + 使用条款 + 用于私密路由 + 消息接收 + 发送 + 用于文件 + 用于消息 + 打开更改 + 打开条款 + 运营方服务器 + 已添加服务器到运营方 %s + 服务器运营方已更改。 + 服务器协议已更改。 + 透明度 + 网络去中心化 + 应用中的第二个预设运营方! + 改进了聊天导航 + 查看更新后的条款 + 比如,如果你通过 SimpleX 服务器收到消息,应用会通过 Flux 服务器传送它们。 + 应用通过在每个对话中使用不同运营方保护你的隐私。 + 接受条款 + 模糊 + 地址还是一次性链接? + 已添加消息服务器 + 已添加媒体和文件服务器 + 地址设置 + 已接受的条款 + 应用工具栏 + 仅用于一名联系人 - 面对面或通过任何消息应用分享.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s的服务器,请接受使用条款。]]> + %s.]]> + 在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。 + 接受条款出错 + 为了更好的元数据隐私。 + 添加服务器出错 + 无媒体和文件服务器。 + 更新服务器出错 + 新服务器 + 无文件接收服务器。 + - 在第一条未读消息上打开聊天.\n- 跳转到引用的消息. + 审阅条款 + SimpleX 地址或一次性链接? + 要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址” + 当前聊天资料的新文件服务器 + 此消息被删除或尚未收到。 + 连接达到了未送达消息上限,你的联系人可能处于离线状态。 + 为了防止链接被替换,你可以比较联系人安全代码。 + 你可以在“网络和服务器”设置中配置运营方。 + 接受运营方条款的日期:%s + 远程移动设备 + 或者导入存档文件 + 小米设备:请在系统设置中开启“自动启动”让通知正常工作。]]> + 消息太大! + 你可以复制并减小消息大小来发送它。 + 请减小消息大小或删除媒体并再次发送。 + 将你的团队成员加入对话。 + 企业地址 + 端到端加密,私信具备后量子密码安全性。]]> + 无后台服务 + 每 10 分钟检查消息 + 它如何帮助隐私 + 应用始终在后台运行 + 通知和电量 + 离开聊天? + 你将停止从这个聊天收到消息。聊天历史将被保留。 + 邀请加入聊天 + 将为你删除聊天 - 此操作无法撤销! + 删除聊天 + 删除聊天? + 添加好友 + 添加团队成员 + 将为所有成员删除聊天 - 此操作无法撤销! + 仅聊天所有人可更改首选项。 + 角色将被更改为 %s。聊天中的每个人都会收到通知。 + 成员之间的私信被禁止。 + 此聊天禁止成员之间的私信。 + 企业聊天 + 客户隐私。 + %1$s连接。]]> + 聊天已存在! + 单手聊天工具栏 + 离开聊天 + 你的聊天个人资料将被发送给聊天成员 + 聊天 + 将从聊天中删除成员 - 此操作无法撤销! + 请减小消息尺寸并再次发送。 + 当启用了超过一个运营方时,没有一个运营方拥有了解谁和谁联络的元数据。 + 已接受邀请 + 被请求连接 + 关于运营方 + SimpleX Chat 和 Flux 达成协议将 Flux 运营的服务器包括在应用中。 + 修复 + 修复连接? + 正进行加密重协商。 + 连接需要加密重协商。 + 保存数据库出错 + 启用日志 + 连接未就绪。 + 创建聊天列表出错 + 更新聊天列表出错 + 加载聊天列表出错 + 列表 + 无聊天 + 找不到聊天 + %s列表中没有聊天。 + 收藏 + + 列表名… + 所有列表的名称和表情符号都应不同。 + 保存列表 + 删除 + 用%s打开 + 将从%s列表删除所有聊天,列表本身也将被删除 + 企业 + 编辑 + 添加到列表 + 联系人 + 创建列表 + 添加列表 + 全部 + 删除列表? + 没有未读聊天 + 附注 + 更改列表 + 更改顺序 + 创建报告出错 + 保存设置出错 + 存档 + 删除举报 + 举报 + 举报其他:仅moderators会看到。 + 举报成员个人资料:仅moderators会看到。 + 举报违规:仅moderators会看到。 + 存档举报 + 举报内容:仅moderators会看到。 + 举报垃圾信息:仅moderators会看到。 + moderators + 另一个理由 + 已存档的举报 + 违反社区指导方针 + 不当内容 + 不当个人资料 + 仅发送人和moderators能看到 + 只有你和moderators能看到 + 垃圾信息 + 存档举报? + 举报理由? + 将为你存档该举报。 + 存档了 %s 的举报 + 举报 + 1 个举报 + 成员举报 + %d 个举报 + 垃圾信息 + 连接被阻止 + 连接被服务器运营方阻止:\n%1$s. + 内容违反使用条款 + 文件被服务器运营方阻止:\n%1$s. + 询问 + + + 打开 web 链接? + 打开链接 + 打开来自聊天列表的链接 + 设置聊天名称… + 停用消息自动删除? + 停用消息删除 + 1 年 + 默认(%s) + 从你的设备删除聊天消息。 + 此操作无法撤销 —— 比此聊天中所选消息更早发出并收到的消息将被删除。 + 此聊天中的消息永远不会被删除。 + 更改消息自动删除设置? + 用于消息收发的 TCP 端口 + 未指定端口时,使用 TCP 端口 %1$s。 + 使用 web 端口 + 全部静音 + 取消提及的已读状态 + 每条消息最多提及 %1$s 名成员! + 将为你存档所有举报。 + 存档所有举报? + 存档 %d 份举报? + 存档举报 + 所有 moderators + 仅自己 + 举报:%s + 禁止向 moderators 举报消息。 + 此群禁止消息举报。 + 成员可以向 moderators 举报消息。 + 允许向 moderators 举报消息。 + 提及成员👋 + 更好的群性能 + 更好的隐私和安全 + 不错过重要消息。 + 更快地删除群。 + 更快发送消息。 + 被提及时收到通知。 + 帮助管理员管理群组。 + 将聊天组织到列表 + 私密媒体文件名。 + 发送私下举报 + 在聊天中设置消息过期时间。 + 被拒绝 + 被拒绝 + 数据库密码短语读取出错 + 无法读取 Keystore 中的密码短语,请手动输入它。这可能在不兼容本应用的系统更新后出现。如果不是这种情况,请联系开发者。 + 无法读取 Keystore 中的密码短语,请手动输入它。这可能在不兼容本应用的系统更新后出现。如果不是这种情况,请联系开发者。 + 待批准 + 待批准 + 条款已更新 + 所有来自这些成员的新消息都将被隐藏! + 为所有其他成员封禁这些成员? + 将从群中移除这些成员 — 此操作无法撤销! + 将显示来自这些成员的消息! + 删除成员吗? + 为所有其他成员解封这些成员吗? + moderators + 将从聊天中移除这些成员 — 此操作无法撤销! + 隐私政策和使用条款。 + 接受 + 使用 SimpleX Chat 代表您同意:\n- 在公开群中只发送合法内容\n- 尊重其他用户 – 没有垃圾信息。 + 服务器运营方无法访问私密聊天、群组和你的联系人。 + 配置服务器运营方 + 不支持的连接链接 + 使用短链接(测试) + SimpleX 频道链接 + 短链接 + 此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。 + 完整链接 + 全部服务器 + 关闭 + 预设服务器 + 仅预设服务器使用 TCP 协议 443 端口。 + 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 c9d4298bc7..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 @@ 管理員 然後,選按: 新增預設伺服器 - 新增伺服器… + 新增伺服器 接受 認證無效 允許 - 顯示名稱: + 個人資料名稱: 全名: - 使用更多電量!通知服務長期在背景中運行 – 有效的訊息就會即時顯示在通知內。]]> - 對電量友善。通知服務於每十分鐘檢查一次訊息。你可能會錯過通話和迫切的訊息。]]> + 使用更多電量!程式始終在背景中運行 – 通知會立即顯示。]]> + 對電量友善。程式每10分鐘檢查一次訊息。你可能會錯過電話或警急訊息。]]> 回應通話請求 清除 允許向群組內的成員傳送私訊。 @@ -69,7 +69,7 @@ 你目前的個人檔案 顯示的名稱中不能有空白。 儲存設定? - 顯示名稱 + 輸入你的名稱: 通話出錯 正在撥打… 通話中 @@ -119,7 +119,6 @@ 聊天室已停止運作 停止 已刪除數據庫的對話內容 - 停止聊天室以啟用數據庫功能。 修改數據庫密碼? 確定要退出群組? 退出 @@ -130,7 +129,7 @@ 建立私密群組 退出群組 已連接 - 群組顯示名稱: + 輸入群組名稱: 群組全名: 對話設定 關閉 @@ -143,8 +142,7 @@ 取消檔案預覽 無法接收檔案 重複的顯示名稱! - 一個單獨的 TCP 連接(和 SOCKS 憑證)將用於每個聯絡人和群組內的成員。 -\n請注意:如果你有很多連接,你的電話電量和數據流量的消耗率會大大增加,一些連接有機會會連接失敗。 + 每個聯絡人和群組內的成員。 \n請注意:如果你有很多連接,你的電話電量和數據流量的消耗率會大大增加,一些連接有機會會連接失敗。]]> 每個聊天室的設定。]]> 返回 省電模式運行中,關閉了背景通知服務和定期更新接收訊息。你可以在通知設定內重新啟用。 @@ -153,7 +151,7 @@ 群組設定 聯絡人設定 分享媒體… - 你和你的聯絡人都可以不可逆地刪除已經傳送的訊息。 + 你和你的聯絡人都可以不可逆地刪除已傳送的訊息。 已連接 簡介 完整連結 @@ -179,13 +177,13 @@ 檢查輸入的伺服器地址,然後再試一次。 終端機對話 於 Github 給個星星 - 匿名聊天模式會保護你的真實個人檔案名稱和頭像 — 當有新聯絡人的時候會自動建立一個隨機性的個人檔案。 + 隱身模式透過為每個聯絡人使用新的隨機設定檔來保護您的隱私。 這樣是允許每一個對話中擁有不同的顯示名稱,並且沒有任何的個人資料可用於分享或有機會外洩。 只有你的聯絡人允許的情況下,才允許自動銷毀訊息。 允許你的聯絡人傳送自動銷毀的訊息。 - 只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。 - 允許你的聯絡人可以不可逆地刪除已發送的訊息。 - 允許將不可撤銷的訊息刪除。 + 只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。(24小時) + 允許你的聯絡人不可逆地刪除已發送的訊息。(24小時) + 允許不可逆地將已傳送的訊息刪除。(24小時) 允許傳送語音訊息。 多久後刪除 群組內所有成員會保持連接。 @@ -231,9 +229,9 @@ 感謝用戶 - 使用 Weblate 的翻譯貢獻! SimpleX k - 透過連結連接聯絡人? - 透過邀請連結連接? - 透過邀請連結連接群組? + 透過聯絡人地址連接? + 透過一次性連結連接? + 加入群組? 你的個人檔案將傳送給你接收此連結的聯絡人。 你將加入此連結內的群組並且連接到此群組成為群組內的成員。 連接 @@ -412,8 +410,6 @@ ICE 伺服器(每行一個) 使用 .onion 主機 Onion 主機不會啟用。 - Onion 主機會在可用時啟用。 - Onion 主機不會啟用。 連接 刪除聯絡地址 顏色 @@ -437,7 +433,6 @@ 請確保你的 WebRTC ICE 伺服器地址是正確的格式,每行也有分隔和沒有重複。 使用直接互聯網連接? 如果你確定,你的訊息伺服器能夠看到你的 IP 位置,和你的網路供應商 - 你正在連接到哪些伺服器。 - 更新 .onion 主機設定? 刪除圖片 開始中 … 等待對方回應… @@ -496,7 +491,6 @@ 需要 Onion 主機會在可用時啟用。 - 連接時將需要使用 Onion 主機。 刪除聯絡地址? 你的個人檔案只會儲存於你的裝置和只會分享給你的聯絡人。 SimpleX 伺服器並不會看到你的個人檔案。 儲存並通知你的聯絡人 @@ -513,7 +507,7 @@ 通話完結 連接 網路 & 伺服器 - 網路設定 + 進階設定 使用 SOCKS 代理伺服器 儲存並通知群組內的聯絡人 退出並且不儲存記錄 @@ -551,13 +545,13 @@ 有一些伺服器測試失敗: 掃描伺服器的二維碼 你的伺服器 - 連接時將需要使用 Onion 主機。 + 連接時需要使用 Onion 主機。 +\n請注意:如果沒有 .onion 地址,您將無法連接到伺服器。 對話檔案 透過群組連結 透過群組連結使用匿名聊天模式 一個使用了匿名聊天模式的人透過連結加入了群組 透過使用一次性連結匿名聊天模式連接 - 如果 SimpleX 沒有任何的用戶標識符,它如何傳送訊息?]]> 即時 定期的 關閉 @@ -602,8 +596,8 @@ 建立連結 刪除連結? 移除成員 - 成員的身份會修改為 \"%s\"。所有在群組內的成員都接收到通知。 - 成員的身份會修改為 \"%s\"。該成員將接收到新的邀請。 + 成員的身份會修改為 "%s"。所有在群組內的成員都接收到通知。 + 成員的身份會修改為 "%s"。該成員將接收到新的邀請。 網路狀態 重置為預設值 當你與某人分享已啟用匿名聊天模式的個人檔案時,此個人檔案將用於他們邀請你參加的群組。 @@ -625,7 +619,7 @@ 沒有聯絡人可以選擇 群組連結 刪除連結 - 群組是完全去中心化的 - 只有群組內的成員能看到。 + 完全去中心化 - 只有成員能看到。 禁止傳送自動銷毀的訊息。 %d 個小時 更新內容 @@ -635,7 +629,6 @@ %s 秒(s) 加密數據庫時出錯 在金鑰庫儲存密碼 - 建立於 %1$s 還原數據庫的備份 群組為不活躍狀態 邀請連結過時! @@ -647,7 +640,7 @@ 儲存群組檔案 語音訊息於這個聊天室是禁用的。 允許你的聯絡人可以完全刪除訊息。 - 第一個沒有任何用戶識別符的通訊平台 – 以私隱為設計。 + 沒有用戶識別符。 新一代的私密訊息平台 去中心化的 人們只能在你分享了連結後,才能和你連接。 @@ -656,10 +649,10 @@ 這是如何運作 你可以之後透過設定修改。 私下連接 - 開放源碼協議和程式碼 – 任何人也可以運行伺服器。 + 任何人都可以託管伺服器。 無視 語音通話來電 - 貼上你接收到的連結 + 貼上你收到的連結 端對端加密 沒有端對端加密 關閉喇叭 @@ -703,8 +696,6 @@ 還原 還原數據庫的備份? 還原數據庫時出錯 - 儲存存檔 - 刪除存檔 加入 確定要加入群組? 加入匿名聊天模式 @@ -760,8 +751,8 @@ 自動銷毀訊息於這個聊天室內是禁用的。 不可逆地刪除訊息於這個聊天室內是禁用的。 只有你可以傳送語音訊息。 - 私訊群組內的成員於這個群組內是禁用的。 - 群組內的成員可以不可逆地刪除訊息。 + 私訊群組內的成員於這個群組內是禁用的。 + 群組內的成員可以不可逆地刪除訊息。(24小時) 語音訊息 改善伺服器配置 當你切換至最近應用程式版面時,無法預覽程式畫面。 @@ -780,7 +771,6 @@ 匯出數據庫時出錯 匯入數據庫時出錯 受加密的數據庫密碼會再次更新。 - 刪除封存對話? 加密數據庫? 邀請至群組 %1$s 邀請成員 @@ -794,7 +784,6 @@ %ds 私人通知 GitHub內查看更多。]]> - 於 GitHub 儲存庫內查看更多。 視訊通話來電 掛斷電話來電 點對點 @@ -802,13 +791,11 @@ 對話沒有經過端對端加密 數據庫已加密! 已加密數據庫 - 封存對話 群組資料已經更新 成員 你:%1$s 刪除群組 即時訊息 - 封存對話 移除成員時出錯 修改身份時出錯 群組 @@ -825,17 +812,15 @@ 自動銷毀訊息於這個群組內是禁用的。 已提供 %s 儲存群組檔案時出錯 - 恢復 主題 - 儲存顏色 你允許 修改群組內的設定 私訊 已啟用 已為你啟用 已為聯絡人啟用 - 只有你能不可逆地刪除訊息(你的聯絡人可以將它標記為刪除)。 - 只有你的聊絡人可以不可逆的刪除訊息(你可以將它標記為刪除)。 + 只有你能不可逆地刪除訊息(你的聯絡人可以將它標記為刪除)。(24小時) + 只有你的聊絡人可以不可逆的刪除訊息(你可以將它標記為刪除)。(24小時) 只有你的聯絡人可以傳送語音訊息。 禁止私訊群組內的成員。 不可逆地刪除訊息於這個群組內是禁用的。 @@ -885,7 +870,6 @@ 更新傳輸隔離模式? 為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 IDs 資料, SimpleX 平台有自家佇列的標識符,這對於你的每個聯絡人也是獨一無二的。 當應用程式是運行中 - 來接收 你的聯絡人訊息 – 這些伺服器用來接收他們傳送給你的訊息。]]> 透過設定啟用於上鎖畫面顯示來電通知。 這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。 你必須在裝置上使用最新版本的對話數據庫,否則你可能會停止接收某些聯絡人的訊息。 @@ -905,7 +889,7 @@ 添加更多身份選項 聯絡人頭像 個人檔案頭像占位符 - 不受垃圾郵件和濫用行為影響 + 不受垃圾和騷擾訊息影響 %1$s 希望透過以下方式聯絡你 開啟視訊 翻轉相機 @@ -914,7 +898,7 @@ 你的通話 經由分程傳遞連接 在上鎖畫面顯示來電通知: - %1$d 你錯過了多個訊息 + %1$d 條訊息已跳過 錯誤的訊息雜湊值 你錯過了多個訊息 @@ -1109,7 +1093,7 @@ %1$d 訊息解密失敗。 使用SOCKS 代理伺服器 你的 XFTP 伺服器 - %1$d 錯過了多個訊息。 + %1$d 條訊息已跳過。 影片和檔案和最大上限為1gb 影片 呈交 @@ -1117,7 +1101,7 @@ 查看更多 SimpleX 聯絡地址 一次性連結 - 主題顏色 + 介面顏色 建立 SimpleX 的聯絡地址 更新了的個人檔案將傳送給你的聯絡人。 與你的聯絡人分享聯絡地址? @@ -1146,7 +1130,7 @@ 已傳送訊息 標題 關於 SimpleX 的聯絡地址 - 外加的顏色 + 額外的強調色 外加的輔助 聯絡地址 後台 @@ -1247,4 +1231,563 @@ \n- 編輯紀錄。 搜尋 已關閉 - \ No newline at end of file + 確認來自未知伺服器的檔案。 + 超出額度 - 收件人未收到先前傳送的訊息 + 應用程式資料轉移 + 應用 + 請在轉移之前確認你還記得數據庫密碼 + 被管理員封鎖 + 進階設定 + 封鎖群組成員 + 活躍連接 + 中止 + 和其他 %d 事件 + 封鎖成員? + 6種全新的介面語言 + 藍芽 + %2$s 審核了 %1$d 條訊息 + 已封鎖 + 將停止地址更改。將使用舊聯絡地址。 + 測試 + 檢查更新 + 相機和麥克風 + 封鎖 + 應用程式主題 + 管理員 + 模糊以增強隱私 + 所有成員 + 管理員可以為所有人封鎖一名成員 + 無法傳送訊息 + 為所有成員封鎖此成員? + + 中止更改地址? + 無法傳送訊息給群組成員 + 所有顏色模式 + 應用到 + 應用程式密碼 + 應用程式 + 聊天顏色 + 聊天已停止。如果你已經在另一台設備使用過此資料庫,你應該在啟動聊天前將數據庫傳輸回來。 + 即將推出! + 軟體更新以下載 + 儲存聯絡人以便稍後聊天 + 相機 + 選擇一個檔案 + 存檔並上傳 + 你的所有聯絡人、對話和檔案將被安全加密並切塊上傳到你設定的 XFTP 中繼 + 正在儲存資料庫 + 取消遷移 + 與 %s 協調加密中… + 允許 + 語音通話 + 相機不可用 + 所有訊息都將被刪除 - 這無法復原 + 請注意:訊息和檔案中繼通過 SOCKS 代理連接。通話和傳送連預覽使用直接連接。]]> + 封鎖全部 + 改進群組功能 + 行動網路 + 封鎖成員 + 警告:此存檔將被刪除。]]> + 清除私密筆記? + 添加聯絡人 + 總是 + 協調加密中… + 允許傳送檔案和媒體 + 允許傳送 SimpleX 連結 + 其他 + 已連接! + 中止更改地址 + 將分享新的隨機個人檔案 + 所有來自 %s 的新訊息都將被隱藏! + 已封鎖 %s + 作者 + 已封鎖 + 被管理員封鎖 + 額外的強調色2 + 阿拉伯語、保加利亞語、芬蘭語、希伯來語、泰國語和烏克蘭語——感謝使用者們與Weblate + 已加入群組! + 確認網路設定 + 嘗試 + 已確認 + 確認錯誤 + 完成 + 區塊已刪除 + 區塊已上傳 + 區塊已下載 + 添加聯絡人: 來創建新的邀請連結,或通過你收到的連結進行連接。]]> + 建立群組: 建立新的群組。]]> + 錯誤的桌面地址 + 已轉移聊天 + 從另一部設備轉移 並掃描QR code。]]> + 請注意: 作為安全保護措施,在兩部設備上使用同一數據庫會破壞解密來自你聯絡人的訊息。]]> + 確定刪除聯絡人? + 檢查更新 + 無法與聯絡人通話 + 通話被禁止! + 無法與群組成員通話 + 應用程式將為新的本機檔案(影片除外)加密。 + 檢查你的網路連接並重試 + 所有配置文件 + 已設定的 SMP 伺服器 + 聊天主題 + 通話 + 允許降級 + 始終使用私密路由。 + 以導出聊天資料庫 + 已設定的 XFTP 伺服器 + 色彩模式 + 已儲存的聯絡人 + 模糊媒體 + 允許通話? + 建立於:%s + Webview 初始化失敗。更新你的系統到新版本。請聯繫開發者。 +\n錯誤:%s + 已刪除聯絡人 + %d 個群事件 + 訊息太大 + 訊息傳送警告 + 錯誤:%1$s + 開發者選項 + 與 %s 的加密需要重協商 + %s 不活躍]]> + 最喜歡 + 訊息成功送達! + 檔案和媒體 + 連線停止 + %s的連接不穩定]]> + 聯絡人 + 適合 + 群組成員可以傳送檔案和媒體。 + 連結行動裝置 + 此群組禁止檔案和媒體 + 結束通話 + 刪除 %d 條訊息嗎? + 訊息草稿 + 允許重新協商加密 + 建立於 + 為所有人封鎖時出錯 + 桌面應用版本 %s 與此應用不相容 + 未找到檔案 - 檔案可能被刪除或被取消了 + 檔案伺服器錯誤:%1$s + 展開 + 啟用(保留組覆蓋) + 淺色 + 淺色模式 + 群組成員可傳送 SimpleX 連結。 + 深色 + 詳情 + 訊息接收 + 無效連結 + %d 條訊息被標記為刪除 + %d 條訊息已攔截 + 轉發伺服器地址不相容網路設定:%1$s。 + 轉發伺服器地址不相容網路設定:%1$s。 + %1$s 的目標伺服器地址不相容轉送伺服器 %2$s 的設定 + %1$s 的目地伺服器版本不相容於轉送伺服器 %2$s. + 關閉通知 + 網路問題 - 多次嘗試傳送訊息後,訊息已過期。 + 轉發伺服器:%1$s +\n目標伺服器錯誤:%2$s + 如果成員變得活躍,可能會在之後傳送訊息。 + 刪除了聯絡人! + 聯絡人將被刪除 - 無法復原此操作 + + 已停用 + 安裝成功 + 建立 + 打開瀏覽器出錯 + 啟用(保留覆蓋) + 禁用(保留組覆蓋) + 為群組啟用回執? + 網路連接 + 繼續 + 檔案狀態 + 深色模式顏色 + 字體大小 + 啟用於 + 立陶宛語使用者介面 + 新的聊天主題 + 連線和伺服器狀態 + 控制你的網路 + 從GitHub下載最新版本。 + 啟用 + 新的聊天體驗 🎉 + 新的媒體選項 + 以連接的行動裝置 + 連接桌面 + 連接到桌面 + 連線終止 + 連接到桌面 + 上傳存檔出錯 + 文件被刪除或鏈接無效 + 導入失敗 + 頭戴式耳機 + 耳機 + 對所有聯絡人關閉 + 深色模式 + 啟用已讀回條時出錯! + (此裝置 v%s)]]> + 不相容的版本 + PC版已斷線 + 轉移裝置 + 通話鈴聲 + 轉發並保存訊息 + 訊息來源保持私密 + 沒有已連接的行動裝置 + 禁止檔案和媒體! + 檔案載入中 + 檔案錯誤 + 備用訊息路由 + 如果你或你的目標伺服器不支持私密路由,將不直接傳送訊息。 + 建立個人資料 + 從另一台裝置轉移 + 成員姓名從 %1$s 改為了 %2$s + 同意加密 + 訊息狀態 + 連接請求將傳送給該組成員。 + - 更穩定的消息傳送。 +\n- 更好的群組。 +\n- 還有更多! + 匈牙利語和土耳其語用戶界面 + 轉移完成 + 從此裝置刪除數據庫 + 傳送 + 下載 + 轉發 + 已轉發 + 轉發自 + 不允許檔案和媒體 + 轉發訊息… + 建立連結中… + 保留 + 聯絡人姓名從 %1$s 改為了 %2$s + 新訊息 + 邀請 + 停用(保留覆蓋) + 訊息狀態:%s + 訊息隊列資訊 + 收到的訊息 + 加密本機檔案 + 檔案 + 對所有群組關閉 + 填充 + 讓你的聊天看上去不相同! + 增大字體大小。 + 輸入密碼短語 + 傳送的訊息 + PC版處理中 + 隱身模式連接 + 已刪除對話! + 目標伺服器錯誤:%1$s + 聊天載入中… + 已轉發的訊息 + 管理員封鎖了 %d 條訊息 + 連結轉發伺服器 %1$s 出錯。請稍候嘗試。 + 刪除資料庫出錯 + 轉發伺服器:%1$s +\n錯誤:%2$s + 轉發伺服器 %1$s 連結目標伺服器 %2$s 失敗。請稍後嘗試。 + 導入存檔中 + 改進訊息傳送 + 安裝更新 + 它保護你的 IP 位址和連線。 + 加入群組對話 + %s 的版本。請檢察兩台裝置安裝的是否版本相同]]> + 更可靠的網路連接 + 發現和加入群組 + 設備 + 新行動裝置 + 保存設定出錯 + 導出的檔案不存在 + 導出資料庫時出錯 + 確認上傳 + 正在建立存檔連結 + 加密OK + 將顯示來自 %s 的訊息! + 送達回執! + 已轉發 + 端對端加密的保護,並具有完全的前向加密、不可否認性和入侵恢復。]]> + 加密協商錯誤 + 抗量子端對端加密保護。]]> + 加密重協商失敗 + 將更新資料庫密碼並儲存在設定中。 + 使用隨機身分建立群組 + 加入速度更快、訊息更可靠。 + 匿名群組 + 連接行動裝置 + 回復 + 和 %1$s 連接? + 在桌面應用裡建立新的帳號。💻 + 輸入此裝置名稱… + 已連結到行動裝置 + 可通過局域網發現 + (新)]]> + 嚴重錯誤 + 內部錯誤 + 驗證密碼短語出錯: + 顯示名稱無效! + 無效的檔案路徑 + 過濾未讀和收藏的聊天記錄。 + 斷開連結 + 斷開桌面連結? + 中止地址更改時出錯 + 顯示通知出錯,請聯繫開發者。 + 刪除並通知聯絡人 + 訊息路由模式 + 不使用私密路由。 + 要進行通話請授予一項或多項權限 + 在 Android 系統設定中找到此權限並手動授予權限。 + 在系統設定中授予 + 傳送邀請出錯 + 建立聯絡人時出錯 + 移除一條訊息 + 保持連接 + - 連接到目錄服務(BETA)! +\n- 發送回執(最多20名成員)。 +\n- 更快,更穩定。 + 連接行動端和桌面端應用程式! 🔗 + 改進訊息傳送 + 已關閉送達回執! + %s 斷開連接]]> + %s 未找到]]> + 存檔下載中 + 轉移中 + 下載失敗 + 為所有組啟用 + 需要重協商加密 + 關閉 + 修復連結 + 修復聯絡人不支援的問題 + 修復 + 修復連結? + 正在進行資料庫轉移。 +\n可能需要幾分鐘時間。 + 建立訊息出錯 + 刪除私密筆記錯誤 + 啟用相機訪問 + 新聊天 + 為所有人啟用 + 同步連接時出錯 + 顯示內容出錯 + 顯示訊息出錯 + 錯誤 + 無後台通話 + 建立聊天資料 + 已同意 %s 的加密 + 允許重新協商與 %s 的加密 + 與 %s 的加密OK + 桌面設備 + 連接桌面選項 + 連接桌面 + 直接連線中 + 邀請 + 建立群組 + 修復群組成員不支援的問題 + 更快地連接到你的好友 + 最多同時刪除20條訊息 + PC版非活躍 + 已連接的伺服器 + 詳細統計數據 + 已停用 + 不活躍 + 直接連接? + 午安! + 早安! + 不向新成員傳送歷史訊息 + 未發送歷史訊息給新成員。 + 在私聊中開啟(測試版)! + 透過QR code轉移到另一部裝置。 + 不啟用 + 錯誤 + 連線停止 + %s的連結斷開,原因是:%s]]> + 斷線原因:%s + 斷開行動裝置連接 + 桌面地址 + 通過局域網發現 + %s 處理中]]> + %s 斷開連接]]> + 下載連結詳情中 + 群組已存在! + 錯誤 + 已下載 + 重製統計數據出錯 + 已過期 + 連接數 + 已建立 + 刪除錯誤 + 解密出錯 + 已刪除 + 已下載的檔案 + 下載出錯 + 功能執行所花費的時間過長:%1$d 秒:%2$s + 無效名稱 + 正確名字為 %s? + 複製錯誤 + 正在連接到桌面 + 找到桌面 + 自動連接 + 與PC版的連接不穩定 + 桌上電腦 + 已安裝的PC版本不支援。請確認兩台裝置所安裝的版本相同 + PC版邀請碼錯誤 + 通過連結連接? + 加入你的群組嗎? + 轉移到此處 + 無效連結 + 在另一部設備上完成轉移 + 下載存檔錯誤 + 轉移到另一部裝置 + 必須停止聊天才能繼續。 + 保留對話 + 不通知刪除 + 無效的QR code + 保留未使用的邀請嗎? + 停用回執? + 啟用回執? + 正在連接聯絡人,請等待或稍後檢查! + 聯絡人被刪除了。 + 傳送調試 + 為儲存的檔案和媒體加密 + 連接到你自己? + 完成轉移 + 目前配置文件 + 檔案 + 重連伺服器出錯 + 重連伺服器出錯 + 重複 + 檔案狀態:%s + 授予權限 + 麥克風 + 資料庫將被加密,密碼將儲存在設定中 + 刪除成員的 %d 條訊息嗎? + 成員非活躍 + 輸入訊息 + 訊息將被標記為刪除。收信人可以揭示這些訊息。 + 連接 + 訊息 + 停用 + 下載更新中,請不要關閉應用 + 下載 %s(%s) + 從不 + 中等 + 媒體和檔案伺服器 + 訊息伺服器 + 即使在對話中禁用。 + 更快的發起聊天 + 修復還原備份後的加密問題 + 網路管理 + 新的桌面應用! + 已連接 + 連接中 + 錯誤 + 為群組停用回執? + 過往的成員 %1$s + 私密訊息路由 🚀 + 貼上存檔連結 + 從桌面使用並掃描QR code。]]> + 無傳送資訊 + 或者顯示此碼 + 待連接 + 請檢查 Simple X 鏈接是否正確。 + 私密筆記 + 請稍後再試。 + 沒有過濾的聯絡人 + 打開檔案位置 + 打開 + 私密筆記 + 波斯語用戶界面 + 從聊天列表播放。 + 正在準備上傳 + 擁有者 + 不相容! + 請將它報告給開發者: +\n%s +\n +\n建議重啟應用。 + 從已連接行動裝置加載檔案時請稍候片刻 + 或者掃描QR code + 禁止傳送 SimpleX 連結 + 沒有選擇聊天 + 打開設定 + 這人資料主題 + 個人資料圖片 + 同一時刻只有一台裝置可工作 + 保護 IP 地址 + 其他 + 無網路連接 + 無歷史記錄 + 無過濾聊天 + 請將它報告給開發者: +\n%s + 打開應用程式設定 + 開啟轉移畫面 + 通知將停止,直到您重啟應用程式 + 禁止傳送檔案和媒體。 + 打開群組 + 只有群組所有者才能啟用檔案和媒體。 + 貼上你收到的連結以與你的聯絡人聯絡… + 先前連接的伺服器 + 其他 + + - 可選擇通知已刪除的聯絡人。 +\n- 帶空格的個人資料名稱。 +\n- 以及更多! + 貼上連結以連接! + 貼上桌面地址 + 請確認此裝置的網路設定是否正確。 + 其他錯誤 + 請檢查行動裝置和桌面設備連接到的是同一個本地網絡,且桌面防火牆允許連接。 +\n請和開發者分享任何其他問題。 + 在防火牆中打開端口 + 或貼上存檔連結 + 正在準備下載 + 只刪除對話 + 貼上連結 + 其他 SMP 伺服器 + 其他 XFTP 伺服器 + 請讓你的聯絡人啟用通話。 + 畫中畫通話 + 或安全分享此文件連結 + 無資訊,試試重新加載 + 打開伺服器設定 + 私密路由出錯 + 尚無直接連接,訊息由管理員轉發。 + 什麼也沒選中 + 打開 + 私密路由 + 打開資料庫文件夾 + 私密訊息路由 + 請重啟應用程式。 + 關閉 + 抗量子加密 + 刪除了聯繫地址 + 重連伺服器? + 接收到的訊息 + 接收錯誤 + 重新連接所有已連接的伺服器來強制傳送訊息。這會使用額外流量。 + 重連伺服器強制傳送訊息。這會使用額外流量。 + 並行接收 + 收件人看不到這條訊息來自誰。 + 刪除了資料圖片 + 可使用的聊天工具箱 + 可存取的聊天工具欄 + 最近歷史和改進的目錄機器人。 + 每 KB 協議超時 + 保護您的真實 IP 地址。不讓你聯絡人選擇的訊息中繼看到它。 +\n在*網絡&伺服器*設定中開啓。 + 隨機密碼以明文形式儲存在設定中。 +\n您可以稍後更改。 + 傳送回條已禁用 + 代理伺服器 + 重連伺服器? + 抗量子端到端加密 + 收到的回覆 + 重連所有伺服器 + 重連 + 代理 + 隨機 + 更新 + 接收總計 + 稍後提醒 + 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 cbdf7a23a3..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,6 +6,15 @@ + + + + +